Parse and validate txt records.
Bug: 27696905
Change-Id: I9affcf02a51c92a2be1c2bfc5efbd09065e100bc
diff --git a/core/java/android/net/nsd/NsdServiceInfo.java b/core/java/android/net/nsd/NsdServiceInfo.java
index 6fdb0d0..4a06fb1 100644
--- a/core/java/android/net/nsd/NsdServiceInfo.java
+++ b/core/java/android/net/nsd/NsdServiceInfo.java
@@ -16,8 +16,11 @@
package android.net.nsd;
+import android.annotation.NonNull;
import android.os.Parcelable;
import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Base64;
import android.util.Log;
import android.util.ArrayMap;
@@ -95,8 +98,99 @@
mPort = p;
}
+ /**
+ * Unpack txt information from a base-64 encoded byte array.
+ *
+ * @param rawRecords The raw base64 encoded records string read from netd.
+ *
+ * @hide
+ */
+ public void setTxtRecords(@NonNull String rawRecords) {
+ byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT);
+
+ // There can be multiple TXT records after each other. Each record has to following format:
+ //
+ // byte type required meaning
+ // ------------------- ------------------- -------- ----------------------------------
+ // 0 unsigned 8 bit yes size of record excluding this byte
+ // 1 - n ASCII but not '=' yes key
+ // n + 1 '=' optional separator of key and value
+ // n + 2 - record size uninterpreted bytes optional value
+ //
+ // Example legal records:
+ // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff]
+ // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '=']
+ // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y']
+ //
+ // Example corrupted records
+ // [3, =, 1, 2] <- key is empty
+ // [3, 0, =, 2] <- key contains non-ASCII character. We handle this by replacing the
+ // invalid characters instead of skipping the record.
+ // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we
+ // handle this by reducing the length of the record as needed.
+ int pos = 0;
+ while (pos < txtRecordsRawBytes.length) {
+ // recordLen is an unsigned 8 bit value
+ int recordLen = txtRecordsRawBytes[pos] & 0xff;
+ pos += 1;
+
+ try {
+ if (recordLen == 0) {
+ throw new IllegalArgumentException("Zero sized txt record");
+ } else if (pos + recordLen > txtRecordsRawBytes.length) {
+ Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen);
+ recordLen = txtRecordsRawBytes.length - pos;
+ }
+
+ // Decode key-value records
+ String key = null;
+ byte[] value = null;
+ int valueLen = 0;
+ for (int i = pos; i < pos + recordLen; i++) {
+ if (key == null) {
+ if (txtRecordsRawBytes[i] == '=') {
+ key = new String(txtRecordsRawBytes, pos, i - pos,
+ StandardCharsets.US_ASCII);
+ }
+ } else {
+ if (value == null) {
+ value = new byte[recordLen - key.length() - 1];
+ }
+ value[valueLen] = txtRecordsRawBytes[i];
+ valueLen++;
+ }
+ }
+
+ // If '=' was not found we have a boolean record
+ if (key == null) {
+ key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII);
+ }
+
+ if (TextUtils.isEmpty(key)) {
+ // Empty keys are not allowed (RFC6763 6.4)
+ throw new IllegalArgumentException("Invalid txt record (key is empty)");
+ }
+
+ if (getAttributes().containsKey(key)) {
+ // When we have a duplicate record, the later ones are ignored (RFC6763 6.4)
+ throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")");
+ }
+
+ setAttribute(key, value);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage());
+ }
+
+ pos += recordLen;
+ }
+ }
+
/** @hide */
public void setAttribute(String key, byte[] value) {
+ if (TextUtils.isEmpty(key)) {
+ throw new IllegalArgumentException("Key cannot be empty");
+ }
+
// Key must be printable US-ASCII, excluding =.
for (int i = 0; i < key.length(); ++i) {
char character = key.charAt(i);
@@ -177,10 +271,10 @@
}
/** @hide */
- public byte[] getTxtRecord() {
+ public @NonNull byte[] getTxtRecord() {
int txtRecordSize = getTxtRecordSize();
if (txtRecordSize == 0) {
- return null;
+ return new byte[]{};
}
byte[] txtRecord = new byte[txtRecordSize];
diff --git a/services/core/java/com/android/server/NsdService.java b/services/core/java/com/android/server/NsdService.java
index 11aef17..a44b065 100644
--- a/services/core/java/com/android/server/NsdService.java
+++ b/services/core/java/com/android/server/NsdService.java
@@ -30,16 +30,14 @@
import android.os.Messenger;
import android.os.UserHandle;
import android.provider.Settings;
+import android.util.Base64;
import android.util.Slog;
import android.util.SparseArray;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
import java.util.concurrent.CountDownLatch;
import com.android.internal.util.AsyncChannel;
@@ -492,6 +490,7 @@
clientInfo.mResolvedService.setServiceName(name);
clientInfo.mResolvedService.setServiceType(type);
clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4]));
+ clientInfo.mResolvedService.setTxtRecords(cooked[6]);
stopResolveService(id);
removeRequestMap(clientId, id, clientInfo);
@@ -708,20 +707,9 @@
if (DBG) Slog.d(TAG, "registerService: " + regId + " " + service);
try {
Command cmd = new Command("mdnssd", "register", regId, service.getServiceName(),
- service.getServiceType(), service.getPort());
-
- // Add TXT records as additional arguments.
- Map<String, byte[]> txtRecords = service.getAttributes();
- for (String key : txtRecords.keySet()) {
- try {
- // TODO: Send encoded TXT record as bytes once NDC/netd supports binary data.
- byte[] recordValue = txtRecords.get(key);
- cmd.appendArg(String.format(Locale.US, "%s=%s", key,
- recordValue != null ? new String(recordValue, "UTF_8") : ""));
- } catch (UnsupportedEncodingException e) {
- Slog.e(TAG, "Failed to encode txtRecord " + e);
- }
- }
+ service.getServiceType(), service.getPort(),
+ Base64.encodeToString(service.getTxtRecord(), Base64.DEFAULT)
+ .replace("\n", ""));
mNativeConnector.execute(cmd);
} catch(NativeDaemonConnectorException e) {