diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index 1001423732..48d40e61bd 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -389,6 +389,7 @@ public final class NsdManager { } private static final int FIRST_LISTENER_KEY = 1; + private static final int DNSSEC_PROTOCOL = 3; private final INsdServiceConnector mService; private final Context mContext; @@ -1754,45 +1755,132 @@ public final class NsdManager { } } + private enum ServiceValidationType { + NO_SERVICE, + HAS_SERVICE, // A service with a positive port + HAS_SERVICE_ZERO_PORT, // A service with a zero port + } + + private enum HostValidationType { + DEFAULT_HOST, // No host is specified so the default host will be used + CUSTOM_HOST, // A custom host with addresses is specified + CUSTOM_HOST_NO_ADDRESS, // A custom host without address is specified + } + + private enum PublicKeyValidationType { + NO_KEY, + HAS_KEY, + } + + /** + * Check if the service is valid for registration and classify it as one of {@link + * ServiceValidationType}. + */ + private static ServiceValidationType validateService(NsdServiceInfo serviceInfo) { + final boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName()); + final boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType()); + if (!hasServiceName && !hasServiceType && serviceInfo.getPort() == 0) { + return ServiceValidationType.NO_SERVICE; + } + if (hasServiceName && hasServiceType) { + if (serviceInfo.getPort() < 0) { + throw new IllegalArgumentException("Invalid port"); + } + if (serviceInfo.getPort() == 0) { + return ServiceValidationType.HAS_SERVICE_ZERO_PORT; + } + return ServiceValidationType.HAS_SERVICE; + } + throw new IllegalArgumentException("The service name or the service type is missing"); + } + + /** + * Check if the host is valid for registration and classify it as one of {@link + * HostValidationType}. + */ + private static HostValidationType validateHost(NsdServiceInfo serviceInfo) { + final boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname()); + final boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses()); + if (!hasHostname) { + // Keep compatible with the legacy behavior: It's allowed to set host + // addresses for a service registration although the host addresses + // won't be registered. To register the addresses for a host, the + // hostname must be specified. + return HostValidationType.DEFAULT_HOST; + } + if (!hasHostAddresses) { + return HostValidationType.CUSTOM_HOST_NO_ADDRESS; + } + return HostValidationType.CUSTOM_HOST; + } + + /** + * Check if the public key is valid for registration and classify it as one of {@link + * PublicKeyValidationType}. + * + *

For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4 + * bytes. See RFC 3445 Section 3. + */ + private static PublicKeyValidationType validatePublicKey(NsdServiceInfo serviceInfo) { + byte[] publicKey = serviceInfo.getPublicKey(); + if (publicKey == null) { + return PublicKeyValidationType.NO_KEY; + } + if (publicKey.length < 4) { + throw new IllegalArgumentException("The public key should be at least 4 bytes long"); + } + int protocol = publicKey[2]; + if (protocol == DNSSEC_PROTOCOL) { + return PublicKeyValidationType.HAS_KEY; + } + throw new IllegalArgumentException( + "The public key's protocol (" + + protocol + + ") is invalid. It should be DNSSEC_PROTOCOL (3)"); + } + /** * Check if the {@link NsdServiceInfo} is valid for registration. * - * The following can be registered: - * - A service with an optional host. - * - A hostname with addresses. + *

Firstly, check if service, host and public key are all valid respectively. Then check if + * the combination of service, host and public key is valid. * - * Note that: - * - When registering a service, the service name, service type and port must be specified. If - * hostname is specified, the host addresses can optionally be specified. - * - When registering a host without a service, the addresses must be specified. + *

If the {@code serviceInfo} is invalid, throw an {@link IllegalArgumentException} + * describing the reason. + * + *

There are the invalid combinations of service, host and public key: + * + *

+ * + *

Keys are used to reserve hostnames or service names while the service/host is temporarily + * inactive, so registrations with a key and just a hostname or a service name are acceptable. * * @hide */ public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) { Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null"); - boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName()); - boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType()); - boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname()); - boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses()); - if (serviceInfo.getPort() < 0) { - throw new IllegalArgumentException("Invalid port"); + final ServiceValidationType serviceValidation = validateService(serviceInfo); + final HostValidationType hostValidation = validateHost(serviceInfo); + final PublicKeyValidationType publicKeyValidation = validatePublicKey(serviceInfo); + + if (serviceValidation == ServiceValidationType.NO_SERVICE + && hostValidation == HostValidationType.DEFAULT_HOST) { + throw new IllegalArgumentException("Nothing to register"); } - - if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) { - if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) { + if (publicKeyValidation == PublicKeyValidationType.NO_KEY) { + if (serviceValidation == ServiceValidationType.HAS_SERVICE_ZERO_PORT) { + throw new IllegalArgumentException("The port is missing"); + } + if (serviceValidation == ServiceValidationType.NO_SERVICE + && hostValidation == HostValidationType.CUSTOM_HOST_NO_ADDRESS) { throw new IllegalArgumentException( - "The service type, service name or port is missing"); + "The host addresses must be specified unless there is a service"); } - } - - if (!hasServiceType && !hasHostname) { - throw new IllegalArgumentException("No service or host specified in NsdServiceInfo"); - } - - if (!hasServiceType && hasHostname && !hasHostAddresses) { - // TODO: b/317946010 - This may be allowed when it supports registering KEY RR. - throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo"); } } } diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java index 9491a9c5c0..2f675a9d0d 100644 --- a/framework-t/src/android/net/nsd/NsdServiceInfo.java +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -37,6 +37,7 @@ import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -68,6 +69,9 @@ public final class NsdServiceInfo implements Parcelable { private int mPort; + @Nullable + private byte[] mPublicKey; + @Nullable private Network mNetwork; @@ -219,6 +223,40 @@ public final class NsdServiceInfo implements Parcelable { mHostname = hostname; } + /** + * Set the public key RDATA to be advertised in a KEY RR (RFC 2535). + * + *

This is the public key of the key pair used for signing a DNS message (e.g. SRP). Clients + * typically don't need this information, but the KEY RR is usually published to claim the use + * of the DNS name so that another mDNS advertiser can't take over the ownership during a + * temporary power down of the original host device. + * + *

When the public key is set to non-null, exactly one KEY RR will be advertised for each of + * the service and host name if they are not null. + * + * @hide // For Thread only + */ + public void setPublicKey(@Nullable byte[] publicKey) { + if (publicKey == null) { + mPublicKey = null; + return; + } + mPublicKey = Arrays.copyOf(publicKey, publicKey.length); + } + + /** + * Get the public key RDATA in the KEY RR (RFC 2535) or {@code null} if no KEY RR exists. + * + * @hide // For Thread only + */ + @Nullable + public byte[] getPublicKey() { + if (mPublicKey == null) { + return null; + } + return Arrays.copyOf(mPublicKey, mPublicKey.length); + } + /** * Unpack txt information from a base-64 encoded byte array. * @@ -622,6 +660,7 @@ public final class NsdServiceInfo implements Parcelable { } dest.writeString(mHostname); dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1); + dest.writeByteArray(mPublicKey); } /** Implement the Parcelable interface */ @@ -654,6 +693,7 @@ public final class NsdServiceInfo implements Parcelable { info.mHostname = in.readString(); final long seconds = in.readLong(); info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds)); + info.mPublicKey = in.createByteArray(); return info; } diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java index 8e89037724..21e34ab1ff 100644 --- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java +++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java @@ -16,6 +16,7 @@ package android.net.nsd; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -51,6 +52,23 @@ public class NsdServiceInfoTest { private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1"); private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::"); + private static final byte[] PUBLIC_KEY_RDATA = new byte[] { + (byte) 0x02, (byte)0x01, // flag + (byte) 0x03, // protocol + (byte) 0x0d, // algorithm + // 64-byte public key below + (byte) 0xC1, (byte) 0x41, (byte) 0xD0, (byte) 0x63, (byte) 0x79, (byte) 0x60, + (byte) 0xB9, (byte) 0x8C, (byte) 0xBC, (byte) 0x12, (byte) 0xCF, (byte) 0xCA, + (byte) 0x22, (byte) 0x1D, (byte) 0x28, (byte) 0x79, (byte) 0xDA, (byte) 0xC2, + (byte) 0x6E, (byte) 0xE5, (byte) 0xB4, (byte) 0x60, (byte) 0xE9, (byte) 0x00, + (byte) 0x7C, (byte) 0x99, (byte) 0x2E, (byte) 0x19, (byte) 0x02, (byte) 0xD8, + (byte) 0x97, (byte) 0xC3, (byte) 0x91, (byte) 0xB0, (byte) 0x37, (byte) 0x64, + (byte) 0xD4, (byte) 0x48, (byte) 0xF7, (byte) 0xD0, (byte) 0xC7, (byte) 0x72, + (byte) 0xFD, (byte) 0xB0, (byte) 0x3B, (byte) 0x1D, (byte) 0x9D, (byte) 0x6D, + (byte) 0x52, (byte) 0xFF, (byte) 0x88, (byte) 0x86, (byte) 0x76, (byte) 0x9E, + (byte) 0x8E, (byte) 0x23, (byte) 0x62, (byte) 0x51, (byte) 0x35, (byte) 0x65, + (byte) 0x27, (byte) 0x09, (byte) 0x62, (byte) 0xD3 + }; @Test public void testLimits() throws Exception { @@ -120,6 +138,7 @@ public class NsdServiceInfoTest { fullInfo.setPort(4242); fullInfo.setHostAddresses(List.of(IPV4_ADDRESS)); fullInfo.setHostname("home"); + fullInfo.setPublicKey(PUBLIC_KEY_RDATA); fullInfo.setNetwork(new Network(123)); fullInfo.setInterfaceIndex(456); checkParcelable(fullInfo); @@ -136,6 +155,7 @@ public class NsdServiceInfoTest { attributedInfo.setPort(4242); attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS)); attributedInfo.setHostname("home"); + attributedInfo.setPublicKey(PUBLIC_KEY_RDATA); attributedInfo.setAttribute("color", "pink"); attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8")); attributedInfo.setAttribute("adorable", (String) null); @@ -172,6 +192,7 @@ public class NsdServiceInfoTest { assertEquals(original.getServiceType(), result.getServiceType()); assertEquals(original.getHost(), result.getHost()); assertEquals(original.getHostname(), result.getHostname()); + assertArrayEquals(original.getPublicKey(), result.getPublicKey()); assertTrue(original.getPort() == result.getPort()); assertEquals(original.getNetwork(), result.getNetwork()); assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex()); diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt index 5ba6c4c720..93cec9cb7d 100644 --- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt +++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt @@ -286,6 +286,12 @@ fun TapPacketReader.pollForQuery( timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS ): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) } +fun TapPacketReader.pollForReply( + recordName: String, + type: Int, + timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS +): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isReplyFor(recordName, type) } + fun TapPacketReader.pollForReply( serviceName: String, serviceType: String, diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt index 6dd4857496..6c6f6a338b 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt @@ -81,7 +81,9 @@ import com.android.compatibility.common.util.PropertyUtil import com.android.compatibility.common.util.SystemUtil import com.android.modules.utils.build.SdkLevel.isAtLeastU import com.android.net.module.util.DnsPacket +import com.android.net.module.util.DnsPacket.ANSECTION import com.android.net.module.util.HexDump +import com.android.net.module.util.HexDump.hexStringToByteArray import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN import com.android.net.module.util.PacketBuilder import com.android.testutils.ConnectivityModuleTest @@ -96,6 +98,7 @@ import com.android.testutils.TapPacketReader import com.android.testutils.TestableNetworkAgent import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated import com.android.testutils.TestableNetworkCallback +import com.android.testutils.assertContainsExactly import com.android.testutils.assertEmpty import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33 @@ -138,6 +141,9 @@ private const val REGISTRATION_TIMEOUT_MS = 10_000L private const val DBG = false private const val TEST_PORT = 12345 private const val MDNS_PORT = 5353.toShort() +private const val TYPE_KEY = 25 +private const val QCLASS_INTERNET = 0x0001 +private const val NAME_RECORDS_TTL_MILLIS: Long = 120 private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address @@ -167,6 +173,12 @@ class NsdManagerTest { private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000)) private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000)) private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000)) + private val publicKey = hexStringToByteArray( + "0201030dc141d0637960b98cbc12cfca" + + "221d2879dac26ee5b460e9007c992e19" + + "02d897c391b03764d448f7d0c772fdb0" + + "3b1d9d6d52ff8886769e8e2362513565" + + "270962d3") private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName) private val ctsNetUtils by lazy{ CtsNetUtils(context) } @@ -2265,6 +2277,165 @@ class NsdManagerTest { } } + @Test + fun testAdvertising_registerServiceAndPublicKey_keyAnnounced() { + val si = NsdServiceInfo().also { + it.network = testNetwork1.network + it.serviceType = serviceType + it.serviceName = serviceName + it.port = TEST_PORT + it.publicKey = publicKey + } + val packetReader = TapPacketReader(Handler(handlerThread.looper), + testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */) + packetReader.startAsyncForTest() + handlerThread.waitForIdle(TIMEOUT_MS) + + val registrationRecord = NsdRegistrationRecord() + val discoveryRecord = NsdDiscoveryRecord() + tryTest { + registerService(registrationRecord, si) + + val announcement = packetReader.pollForReply( + "$serviceName.$serviceType.local", + TYPE_KEY + ) + assertNotNull(announcement) + val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY } + assertEquals(1, keyRecords.size) + val actualRecord = keyRecords.get(0) + assertEquals(TYPE_KEY, actualRecord.nsType) + assertEquals("$serviceName.$serviceType.local", actualRecord.dName) + assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl) + assertArrayEquals(publicKey, actualRecord.rr) + + nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, + testNetwork1.network, Executor { it.run() }, discoveryRecord) + + val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered( + serviceName, serviceType, testNetwork1.network) + val resolvedInfo1 = resolveService(discoveredInfo1) + + assertEquals(serviceName, discoveredInfo1.serviceName) + assertEquals(TEST_PORT, resolvedInfo1.port) + } cleanupStep { + nsdManager.stopServiceDiscovery(discoveryRecord) + + discoveryRecord.expectCallback() + } cleanup { + nsdManager.unregisterService(registrationRecord) + } + } + + @Test + fun testAdvertising_registerCustomHostAndPublicKey_keyAnnounced() { + val si = NsdServiceInfo().also { + it.network = testNetwork1.network + it.hostname = customHostname + it.hostAddresses = listOf( + parseNumericAddress("192.0.2.23"), + parseNumericAddress("2001:db8::1"), + parseNumericAddress("2001:db8::2")) + it.publicKey = publicKey + } + val packetReader = TapPacketReader(Handler(handlerThread.looper), + testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */) + packetReader.startAsyncForTest() + handlerThread.waitForIdle(TIMEOUT_MS) + + val registrationRecord = NsdRegistrationRecord() + tryTest { + registerService(registrationRecord, si) + + val announcement = packetReader.pollForReply("$customHostname.local", TYPE_KEY) + assertNotNull(announcement) + val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY } + assertEquals(1, keyRecords.size) + val actualRecord = keyRecords.get(0) + assertEquals(TYPE_KEY, actualRecord.nsType) + assertEquals("$customHostname.local", actualRecord.dName) + assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl) + assertArrayEquals(publicKey, actualRecord.rr) + + // This test case focuses on key announcement so we don't check the details of the + // announcement of the custom host addresses. + val addressRecords = announcement.records[ANSECTION].filter { + it.nsType == DnsResolver.TYPE_AAAA || + it.nsType == DnsResolver.TYPE_A + } + assertEquals(3, addressRecords.size) + } cleanup { + nsdManager.unregisterService(registrationRecord) + } + } + + @Test + fun testAdvertising_registerTwoServicesWithSameCustomHostAndPublicKey_keyAnnounced() { + val si1 = NsdServiceInfo().also { + it.network = testNetwork1.network + it.serviceType = serviceType + it.serviceName = serviceName + it.port = TEST_PORT + it.hostname = customHostname + it.hostAddresses = listOf( + parseNumericAddress("192.0.2.23"), + parseNumericAddress("2001:db8::1"), + parseNumericAddress("2001:db8::2")) + it.publicKey = publicKey + } + val si2 = NsdServiceInfo().also { + it.network = testNetwork1.network + it.serviceType = serviceType2 + it.serviceName = serviceName2 + it.port = TEST_PORT + 1 + it.hostname = customHostname + it.hostAddresses = listOf() + it.publicKey = publicKey + } + val packetReader = TapPacketReader(Handler(handlerThread.looper), + testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */) + packetReader.startAsyncForTest() + handlerThread.waitForIdle(TIMEOUT_MS) + + val registrationRecord1 = NsdRegistrationRecord() + val registrationRecord2 = NsdRegistrationRecord() + tryTest { + registerService(registrationRecord1, si1) + + var announcement = + packetReader.pollForReply("$serviceName.$serviceType.local", TYPE_KEY) + assertNotNull(announcement) + var keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY } + assertEquals(2, keyRecords.size) + assertTrue(keyRecords.any { it.dName == "$serviceName.$serviceType.local" }) + assertTrue(keyRecords.any { it.dName == "$customHostname.local" }) + assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS }) + assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) }) + + // This test case focuses on key announcement so we don't check the details of the + // announcement of the custom host addresses. + val addressRecords = announcement.records[ANSECTION].filter { + it.nsType == DnsResolver.TYPE_AAAA || + it.nsType == DnsResolver.TYPE_A + } + assertEquals(3, addressRecords.size) + + registerService(registrationRecord2, si2) + + announcement = packetReader.pollForReply("$serviceName2.$serviceType2.local", TYPE_KEY) + assertNotNull(announcement) + keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY } + assertEquals(2, keyRecords.size) + assertTrue(keyRecords.any { it.dName == "$serviceName2.$serviceType2.local" }) + assertTrue(keyRecords.any { it.dName == "$customHostname.local" }) + assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS }) + assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) }) + } cleanup { + nsdManager.unregisterService(registrationRecord1) + nsdManager.unregisterService(registrationRecord2) + } + } + @Test fun testServiceTypeClientRemovedAfterSocketDestroyed() { val si = makeTestServiceInfo(testNetwork1.network) diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java index 27c45618ea..9c812a1b9e 100644 --- a/tests/unit/java/android/net/nsd/NsdManagerTest.java +++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java @@ -16,6 +16,11 @@ package android.net.nsd; +import static android.net.InetAddresses.parseNumericAddress; +import static android.net.nsd.NsdManager.checkServiceInfoForRegistration; + +import static com.android.net.module.util.HexDump.hexStringToByteArray; + import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; @@ -54,6 +59,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.net.InetAddress; +import java.util.Collections; import java.util.List; import java.time.Duration; @@ -395,6 +401,7 @@ public class NsdManagerTest { NsdManager.RegistrationListener listener4 = mock(NsdManager.RegistrationListener.class); NsdManager.RegistrationListener listener5 = mock(NsdManager.RegistrationListener.class); NsdManager.RegistrationListener listener6 = mock(NsdManager.RegistrationListener.class); + NsdManager.RegistrationListener listener7 = mock(NsdManager.RegistrationListener.class); NsdServiceInfo invalidService = new NsdServiceInfo(null, null); NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp"); @@ -439,6 +446,19 @@ public class NsdManagerTest { validServiceWithCustomHostNoAddresses.setPort(2222); validServiceWithCustomHostNoAddresses.setHostname("a_host"); + NsdServiceInfo validServiceWithPublicKey = new NsdServiceInfo("a_name", "_a_type._tcp"); + validServiceWithPublicKey.setPublicKey( + hexStringToByteArray( + "0201030dc141d0637960b98cbc12cfca" + + "221d2879dac26ee5b460e9007c992e19" + + "02d897c391b03764d448f7d0c772fdb0" + + "3b1d9d6d52ff8886769e8e2362513565" + + "270962d3")); + + NsdServiceInfo invalidServiceWithTooShortPublicKey = + new NsdServiceInfo("a_name", "_a_type._tcp"); + invalidServiceWithTooShortPublicKey.setPublicKey(hexStringToByteArray("0201")); + // Service registration // - invalid arguments mustFail(() -> { manager.unregisterService(null); }); @@ -449,6 +469,8 @@ public class NsdManagerTest { mustFail(() -> { manager.registerService(validService, PROTOCOL, null); }); mustFail(() -> { manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); }); + mustFail(() -> { + manager.registerService(invalidServiceWithTooShortPublicKey, PROTOCOL, listener1); }); manager.registerService(validService, PROTOCOL, listener1); // - update without subtype is not allowed mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); }); @@ -479,6 +501,9 @@ public class NsdManagerTest { // - registering a service with a custom host with no addresses is valid manager.registerService(validServiceWithCustomHostNoAddresses, PROTOCOL, listener6); manager.unregisterService(listener6); + // - registering a service with a public key is valid + manager.registerService(validServiceWithPublicKey, PROTOCOL, listener7); + manager.unregisterService(listener7); // Discover service // - invalid arguments @@ -506,6 +531,229 @@ public class NsdManagerTest { mustFail(() -> { manager.resolveService(validService, listener3); }); } + private static final class NsdServiceInfoBuilder { + private static final String SERVICE_NAME = "TestService"; + private static final String SERVICE_TYPE = "_testservice._tcp"; + private static final int SERVICE_PORT = 12345; + private static final String HOSTNAME = "TestHost"; + private static final List HOST_ADDRESSES = + List.of(parseNumericAddress("192.168.2.23"), parseNumericAddress("2001:db8::3")); + private static final byte[] PUBLIC_KEY = + hexStringToByteArray( + "0201030dc141d0637960b98cbc12cfca" + + "221d2879dac26ee5b460e9007c992e19" + + "02d897c391b03764d448f7d0c772fdb0" + + "3b1d9d6d52ff8886769e8e2362513565" + + "270962d3"); + + private final NsdServiceInfo mNsdServiceInfo = new NsdServiceInfo(); + + NsdServiceInfo build() { + return mNsdServiceInfo; + } + + NsdServiceInfoBuilder setNoService() { + mNsdServiceInfo.setServiceName(null); + mNsdServiceInfo.setServiceType(null); + mNsdServiceInfo.setPort(0); + return this; + } + + NsdServiceInfoBuilder setService() { + mNsdServiceInfo.setServiceName(SERVICE_NAME); + mNsdServiceInfo.setServiceType(SERVICE_TYPE); + mNsdServiceInfo.setPort(SERVICE_PORT); + return this; + } + + NsdServiceInfoBuilder setZeroPortService() { + mNsdServiceInfo.setServiceName(SERVICE_NAME); + mNsdServiceInfo.setServiceType(SERVICE_TYPE); + mNsdServiceInfo.setPort(0); + return this; + } + + NsdServiceInfoBuilder setInvalidService() { + mNsdServiceInfo.setServiceName(SERVICE_NAME); + mNsdServiceInfo.setServiceType(null); + mNsdServiceInfo.setPort(SERVICE_PORT); + return this; + } + + NsdServiceInfoBuilder setDefaultHost() { + mNsdServiceInfo.setHostname(null); + mNsdServiceInfo.setHostAddresses(Collections.emptyList()); + return this; + } + + NsdServiceInfoBuilder setCustomHost() { + mNsdServiceInfo.setHostname(HOSTNAME); + mNsdServiceInfo.setHostAddresses(HOST_ADDRESSES); + return this; + } + + NsdServiceInfoBuilder setCustomHostNoAddress() { + mNsdServiceInfo.setHostname(HOSTNAME); + mNsdServiceInfo.setHostAddresses(Collections.emptyList()); + return this; + } + + NsdServiceInfoBuilder setHostAddressesNoHostname() { + mNsdServiceInfo.setHostname(null); + mNsdServiceInfo.setHostAddresses(HOST_ADDRESSES); + return this; + } + + NsdServiceInfoBuilder setNoPublicKey() { + mNsdServiceInfo.setPublicKey(null); + return this; + } + + NsdServiceInfoBuilder setPublicKey() { + mNsdServiceInfo.setPublicKey(PUBLIC_KEY); + return this; + } + + NsdServiceInfoBuilder setInvalidPublicKey() { + mNsdServiceInfo.setPublicKey(new byte[3]); + return this; + } + } + + @Test + public void testCheckServiceInfoForRegistration() { + // The service is invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setInvalidService() + .setCustomHost() + .setPublicKey().build())); + // Keep compatible with the legacy behavior: It's allowed to set host + // addresses for a service registration although the host addresses + // won't be registered. To register the addresses for a host, the + // hostname must be specified. + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setHostAddressesNoHostname() + .setPublicKey().build()); + // The public key is invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setCustomHost() + .setInvalidPublicKey().build())); + // Invalid combinations + // 1. (service, custom host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setCustomHost() + .setPublicKey().build()); + // 2. (service, custom host, no key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setCustomHost() + .setNoPublicKey().build()); + // 3. (service, no-address custom host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setCustomHostNoAddress() + .setPublicKey().build()); + // 4. (service, no-address custom host, no key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setCustomHostNoAddress() + .setNoPublicKey().build()); + // 5. (service, default host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setDefaultHost() + .setPublicKey().build()); + // 6. (service, default host, no key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setService() + .setDefaultHost() + .setNoPublicKey().build()); + // 7. (0-port service, custom host, valid key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setZeroPortService() + .setCustomHost() + .setPublicKey().build()); + // 8. (0-port service, custom host, no key): invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setZeroPortService() + .setCustomHost() + .setNoPublicKey().build())); + // 9. (0-port service, no-address custom host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setZeroPortService() + .setCustomHostNoAddress() + .setPublicKey().build()); + // 10. (0-port service, no-address custom host, no key): invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setZeroPortService() + .setCustomHostNoAddress() + .setNoPublicKey().build())); + // 11. (0-port service, default host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setZeroPortService() + .setDefaultHost() + .setPublicKey().build()); + // 12. (0-port service, default host, no key): invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setZeroPortService() + .setDefaultHost() + .setNoPublicKey().build())); + // 13. (no service, custom host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setNoService() + .setCustomHost() + .setPublicKey().build()); + // 14. (no service, custom host, no key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setNoService() + .setCustomHost() + .setNoPublicKey().build()); + // 15. (no service, no-address custom host, key): valid + checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setNoService() + .setCustomHostNoAddress() + .setPublicKey().build()); + // 16. (no service, no-address custom host, no key): invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setNoService() + .setCustomHostNoAddress() + .setNoPublicKey().build())); + // 17. (no service, default host, key): invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setNoService() + .setDefaultHost() + .setPublicKey().build())); + // 18. (no service, default host, no key): invalid + mustFail(() -> checkServiceInfoForRegistration( + new NsdServiceInfoBuilder() + .setNoService() + .setDefaultHost() + .setNoPublicKey().build())); + } + public void mustFail(Runnable fn) { try { fn.run();