• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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 android.net.nsd;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.net.Network;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.text.TextUtils;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 
29 import com.android.net.module.util.InetAddressUtils;
30 
31 import java.io.UnsupportedEncodingException;
32 import java.net.InetAddress;
33 import java.nio.charset.StandardCharsets;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.List;
37 import java.util.Map;
38 
39 /**
40  * A class representing service information for network service discovery
41  * @see NsdManager
42  */
43 public final class NsdServiceInfo implements Parcelable {
44 
45     private static final String TAG = "NsdServiceInfo";
46 
47     private String mServiceName;
48 
49     private String mServiceType;
50 
51     private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
52 
53     private final List<InetAddress> mHostAddresses = new ArrayList<>();
54 
55     private int mPort;
56 
57     @Nullable
58     private Network mNetwork;
59 
60     private int mInterfaceIndex;
61 
NsdServiceInfo()62     public NsdServiceInfo() {
63     }
64 
65     /** @hide */
NsdServiceInfo(String sn, String rt)66     public NsdServiceInfo(String sn, String rt) {
67         mServiceName = sn;
68         mServiceType = rt;
69     }
70 
71     /** Get the service name */
getServiceName()72     public String getServiceName() {
73         return mServiceName;
74     }
75 
76     /** Set the service name */
setServiceName(String s)77     public void setServiceName(String s) {
78         mServiceName = s;
79     }
80 
81     /** Get the service type */
getServiceType()82     public String getServiceType() {
83         return mServiceType;
84     }
85 
86     /** Set the service type */
setServiceType(String s)87     public void setServiceType(String s) {
88         mServiceType = s;
89     }
90 
91     /**
92      * Get the host address. The host address is valid for a resolved service.
93      *
94      * @deprecated Use {@link #getHostAddresses()} to get the entire list of addresses for the host.
95      */
96     @Deprecated
getHost()97     public InetAddress getHost() {
98         return mHostAddresses.size() == 0 ? null : mHostAddresses.get(0);
99     }
100 
101     /**
102      * Set the host address
103      *
104      * @deprecated Use {@link #setHostAddresses(List)} to set multiple addresses for the host.
105      */
106     @Deprecated
setHost(InetAddress s)107     public void setHost(InetAddress s) {
108         setHostAddresses(Collections.singletonList(s));
109     }
110 
111     /**
112      * Get port number. The port number is valid for a resolved service.
113      *
114      * The port is valid for all addresses.
115      * @see #getHostAddresses()
116      */
getPort()117     public int getPort() {
118         return mPort;
119     }
120 
121     /** Set port number */
setPort(int p)122     public void setPort(int p) {
123         mPort = p;
124     }
125 
126     /**
127      * Get the host addresses.
128      *
129      * All host addresses are valid for the resolved service.
130      * All addresses share the same port
131      * @see #getPort()
132      */
133     @NonNull
getHostAddresses()134     public List<InetAddress> getHostAddresses() {
135         return new ArrayList<>(mHostAddresses);
136     }
137 
138     /** Set the host addresses */
setHostAddresses(@onNull List<InetAddress> addresses)139     public void setHostAddresses(@NonNull List<InetAddress> addresses) {
140         mHostAddresses.clear();
141         mHostAddresses.addAll(addresses);
142     }
143 
144     /**
145      * Unpack txt information from a base-64 encoded byte array.
146      *
147      * @param txtRecordsRawBytes The raw base64 encoded byte array.
148      *
149      * @hide
150      */
setTxtRecords(@onNull byte[] txtRecordsRawBytes)151     public void setTxtRecords(@NonNull byte[] txtRecordsRawBytes) {
152         // There can be multiple TXT records after each other. Each record has to following format:
153         //
154         // byte                  type                  required   meaning
155         // -------------------   -------------------   --------   ----------------------------------
156         // 0                     unsigned 8 bit        yes        size of record excluding this byte
157         // 1 - n                 ASCII but not '='     yes        key
158         // n + 1                 '='                   optional   separator of key and value
159         // n + 2 - record size   uninterpreted bytes   optional   value
160         //
161         // Example legal records:
162         // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff]
163         // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '=']
164         // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y']
165         //
166         // Example corrupted records
167         // [3, =, 1, 2]    <- key is empty
168         // [3, 0, =, 2]    <- key contains non-ASCII character. We handle this by replacing the
169         //                    invalid characters instead of skipping the record.
170         // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we
171         //                    handle this by reducing the length of the record as needed.
172         int pos = 0;
173         while (pos < txtRecordsRawBytes.length) {
174             // recordLen is an unsigned 8 bit value
175             int recordLen = txtRecordsRawBytes[pos] & 0xff;
176             pos += 1;
177 
178             try {
179                 if (recordLen == 0) {
180                     throw new IllegalArgumentException("Zero sized txt record");
181                 } else if (pos + recordLen > txtRecordsRawBytes.length) {
182                     Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen);
183                     recordLen = txtRecordsRawBytes.length - pos;
184                 }
185 
186                 // Decode key-value records
187                 String key = null;
188                 byte[] value = null;
189                 int valueLen = 0;
190                 for (int i = pos; i < pos + recordLen; i++) {
191                     if (key == null) {
192                         if (txtRecordsRawBytes[i] == '=') {
193                             key = new String(txtRecordsRawBytes, pos, i - pos,
194                                     StandardCharsets.US_ASCII);
195                         }
196                     } else {
197                         if (value == null) {
198                             value = new byte[recordLen - key.length() - 1];
199                         }
200                         value[valueLen] = txtRecordsRawBytes[i];
201                         valueLen++;
202                     }
203                 }
204 
205                 // If '=' was not found we have a boolean record
206                 if (key == null) {
207                     key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII);
208                 }
209 
210                 if (TextUtils.isEmpty(key)) {
211                     // Empty keys are not allowed (RFC6763 6.4)
212                     throw new IllegalArgumentException("Invalid txt record (key is empty)");
213                 }
214 
215                 if (getAttributes().containsKey(key)) {
216                     // When we have a duplicate record, the later ones are ignored (RFC6763 6.4)
217                     throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")");
218                 }
219 
220                 setAttribute(key, value);
221             } catch (IllegalArgumentException e) {
222                 Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage());
223             }
224 
225             pos += recordLen;
226         }
227     }
228 
229     /** @hide */
230     @UnsupportedAppUsage
setAttribute(String key, byte[] value)231     public void setAttribute(String key, byte[] value) {
232         if (TextUtils.isEmpty(key)) {
233             throw new IllegalArgumentException("Key cannot be empty");
234         }
235 
236         // Key must be printable US-ASCII, excluding =.
237         for (int i = 0; i < key.length(); ++i) {
238             char character = key.charAt(i);
239             if (character < 0x20 || character > 0x7E) {
240                 throw new IllegalArgumentException("Key strings must be printable US-ASCII");
241             } else if (character == 0x3D) {
242                 throw new IllegalArgumentException("Key strings must not include '='");
243             }
244         }
245 
246         // Key length + value length must be < 255.
247         if (key.length() + (value == null ? 0 : value.length) >= 255) {
248             throw new IllegalArgumentException("Key length + value length must be < 255 bytes");
249         }
250 
251         // Warn if key is > 9 characters, as recommended by RFC 6763 section 6.4.
252         if (key.length() > 9) {
253             Log.w(TAG, "Key lengths > 9 are discouraged: " + key);
254         }
255 
256         // Check against total TXT record size limits.
257         // Arbitrary 400 / 1300 byte limits taken from RFC 6763 section 6.2.
258         int txtRecordSize = getTxtRecordSize();
259         int futureSize = txtRecordSize + key.length() + (value == null ? 0 : value.length) + 2;
260         if (futureSize > 1300) {
261             throw new IllegalArgumentException("Total length of attributes must be < 1300 bytes");
262         } else if (futureSize > 400) {
263             Log.w(TAG, "Total length of all attributes exceeds 400 bytes; truncation may occur");
264         }
265 
266         mTxtRecord.put(key, value);
267     }
268 
269     /**
270      * Add a service attribute as a key/value pair.
271      *
272      * <p> Service attributes are included as DNS-SD TXT record pairs.
273      *
274      * <p> The key must be US-ASCII printable characters, excluding the '=' character.  Values may
275      * be UTF-8 strings or null.  The total length of key + value must be less than 255 bytes.
276      *
277      * <p> Keys should be short, ideally no more than 9 characters, and unique per instance of
278      * {@link NsdServiceInfo}.  Calling {@link #setAttribute} twice with the same key will overwrite
279      * first value.
280      */
setAttribute(String key, String value)281     public void setAttribute(String key, String value) {
282         try {
283             setAttribute(key, value == null ? (byte []) null : value.getBytes("UTF-8"));
284         } catch (UnsupportedEncodingException e) {
285             throw new IllegalArgumentException("Value must be UTF-8");
286         }
287     }
288 
289     /** Remove an attribute by key */
removeAttribute(String key)290     public void removeAttribute(String key) {
291         mTxtRecord.remove(key);
292     }
293 
294     /**
295      * Retrieve attributes as a map of String keys to byte[] values. The attributes map is only
296      * valid for a resolved service.
297      *
298      * <p> The returned map is unmodifiable; changes must be made through {@link #setAttribute} and
299      * {@link #removeAttribute}.
300      */
getAttributes()301     public Map<String, byte[]> getAttributes() {
302         return Collections.unmodifiableMap(mTxtRecord);
303     }
304 
getTxtRecordSize()305     private int getTxtRecordSize() {
306         int txtRecordSize = 0;
307         for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
308             txtRecordSize += 2;  // One for the length byte, one for the = between key and value.
309             txtRecordSize += entry.getKey().length();
310             byte[] value = entry.getValue();
311             txtRecordSize += value == null ? 0 : value.length;
312         }
313         return txtRecordSize;
314     }
315 
316     /** @hide */
getTxtRecord()317     public @NonNull byte[] getTxtRecord() {
318         int txtRecordSize = getTxtRecordSize();
319         if (txtRecordSize == 0) {
320             return new byte[]{};
321         }
322 
323         byte[] txtRecord = new byte[txtRecordSize];
324         int ptr = 0;
325         for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
326             String key = entry.getKey();
327             byte[] value = entry.getValue();
328 
329             // One byte to record the length of this key/value pair.
330             txtRecord[ptr++] = (byte) (key.length() + (value == null ? 0 : value.length) + 1);
331 
332             // The key, in US-ASCII.
333             // Note: use the StandardCharsets const here because it doesn't raise exceptions and we
334             // already know the key is ASCII at this point.
335             System.arraycopy(key.getBytes(StandardCharsets.US_ASCII), 0, txtRecord, ptr,
336                     key.length());
337             ptr += key.length();
338 
339             // US-ASCII '=' character.
340             txtRecord[ptr++] = (byte)'=';
341 
342             // The value, as any raw bytes.
343             if (value != null) {
344                 System.arraycopy(value, 0, txtRecord, ptr, value.length);
345                 ptr += value.length;
346             }
347         }
348         return txtRecord;
349     }
350 
351     /**
352      * Get the network where the service can be found.
353      *
354      * This is set if this {@link NsdServiceInfo} was obtained from
355      * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}, unless the service
356      * was found on a network interface that does not have a {@link Network} (such as a tethering
357      * downstream, where services are advertised from devices connected to this device via
358      * tethering).
359      */
360     @Nullable
getNetwork()361     public Network getNetwork() {
362         return mNetwork;
363     }
364 
365     /**
366      * Set the network where the service can be found.
367      * @param network The network, or null to search for, or to announce, the service on all
368      *                connected networks.
369      */
setNetwork(@ullable Network network)370     public void setNetwork(@Nullable Network network) {
371         mNetwork = network;
372     }
373 
374     /**
375      * Get the index of the network interface where the service was found.
376      *
377      * This is only set when the service was found on an interface that does not have a usable
378      * Network, in which case {@link #getNetwork()} returns null.
379      * @return The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
380      * @hide
381      */
getInterfaceIndex()382     public int getInterfaceIndex() {
383         return mInterfaceIndex;
384     }
385 
386     /**
387      * Set the index of the network interface where the service was found.
388      * @hide
389      */
setInterfaceIndex(int interfaceIndex)390     public void setInterfaceIndex(int interfaceIndex) {
391         mInterfaceIndex = interfaceIndex;
392     }
393 
394     @Override
toString()395     public String toString() {
396         StringBuilder sb = new StringBuilder();
397         sb.append("name: ").append(mServiceName)
398                 .append(", type: ").append(mServiceType)
399                 .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
400                 .append(", port: ").append(mPort)
401                 .append(", network: ").append(mNetwork);
402 
403         byte[] txtRecord = getTxtRecord();
404         sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
405         return sb.toString();
406     }
407 
408     /** Implement the Parcelable interface */
describeContents()409     public int describeContents() {
410         return 0;
411     }
412 
413     /** Implement the Parcelable interface */
writeToParcel(Parcel dest, int flags)414     public void writeToParcel(Parcel dest, int flags) {
415         dest.writeString(mServiceName);
416         dest.writeString(mServiceType);
417         dest.writeInt(mPort);
418 
419         // TXT record key/value pairs.
420         dest.writeInt(mTxtRecord.size());
421         for (String key : mTxtRecord.keySet()) {
422             byte[] value = mTxtRecord.get(key);
423             if (value != null) {
424                 dest.writeInt(1);
425                 dest.writeInt(value.length);
426                 dest.writeByteArray(value);
427             } else {
428                 dest.writeInt(0);
429             }
430             dest.writeString(key);
431         }
432 
433         dest.writeParcelable(mNetwork, 0);
434         dest.writeInt(mInterfaceIndex);
435         dest.writeInt(mHostAddresses.size());
436         for (InetAddress address : mHostAddresses) {
437             InetAddressUtils.parcelInetAddress(dest, address, flags);
438         }
439     }
440 
441     /** Implement the Parcelable interface */
442     public static final @android.annotation.NonNull Creator<NsdServiceInfo> CREATOR =
443         new Creator<NsdServiceInfo>() {
444             public NsdServiceInfo createFromParcel(Parcel in) {
445                 NsdServiceInfo info = new NsdServiceInfo();
446                 info.mServiceName = in.readString();
447                 info.mServiceType = in.readString();
448                 info.mPort = in.readInt();
449 
450                 // TXT record key/value pairs.
451                 int recordCount = in.readInt();
452                 for (int i = 0; i < recordCount; ++i) {
453                     byte[] valueArray = null;
454                     if (in.readInt() == 1) {
455                         int valueLength = in.readInt();
456                         valueArray = new byte[valueLength];
457                         in.readByteArray(valueArray);
458                     }
459                     info.mTxtRecord.put(in.readString(), valueArray);
460                 }
461                 info.mNetwork = in.readParcelable(null, Network.class);
462                 info.mInterfaceIndex = in.readInt();
463                 int size = in.readInt();
464                 for (int i = 0; i < size; i++) {
465                     info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
466                 }
467                 return info;
468             }
469 
470             public NsdServiceInfo[] newArray(int size) {
471                 return new NsdServiceInfo[size];
472             }
473         };
474 }
475