Add mdns files and unit tests
- Put mdns code to service/mdns because we don't want to impact coverage for code that's not yet in use. - Also fix some lint errors - Common typo errors - Lines longer than 100 characters - Update IgnoreUpTo from S to SC_V2 because the tests should be ignored until T. Bug: 232766079 Test: atest FrameworksNetTests Change-Id: I19af3bf7ae004bd37960f2ad9014cf0c6a804c8d
This commit is contained in:
parent
522cea7d4e
commit
3e1f4489bf
38 changed files with 7249 additions and 0 deletions
|
|
@ -181,6 +181,25 @@ java_library {
|
|||
],
|
||||
}
|
||||
|
||||
// TODO: Remove this temporary library and put code into module when test coverage is enough.
|
||||
java_library {
|
||||
name: "service-mdns",
|
||||
sdk_version: "system_server_current",
|
||||
min_sdk_version: "30",
|
||||
srcs: [
|
||||
"mdns/**/*.java",
|
||||
],
|
||||
libs: [
|
||||
"framework-annotations-lib",
|
||||
"framework-connectivity-pre-jarjar",
|
||||
"framework-wifi.stubs.module_lib",
|
||||
"service-connectivity-pre-jarjar",
|
||||
],
|
||||
visibility: [
|
||||
"//packages/modules/Connectivity/tests:__subpackages__",
|
||||
],
|
||||
}
|
||||
|
||||
java_library {
|
||||
name: "service-connectivity-protos",
|
||||
sdk_version: "system_current",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
/** Interface for monitoring connectivity changes. */
|
||||
public interface ConnectivityMonitor {
|
||||
/**
|
||||
* Starts monitoring changes of connectivity of this device, which may indicate that the list of
|
||||
* network interfaces available for multi-cast messaging has changed.
|
||||
*/
|
||||
void startWatchingConnectivityChanges();
|
||||
|
||||
/** Stops monitoring changes of connectivity. */
|
||||
void stopWatchingConnectivityChanges();
|
||||
|
||||
void notifyConnectivityChange();
|
||||
|
||||
/** Listener interface for receiving connectivity changes. */
|
||||
interface Listener {
|
||||
void onConnectivityChanged();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.os.Build;
|
||||
|
||||
import com.android.server.connectivity.mdns.util.MdnsLogger;
|
||||
|
||||
/** Class for monitoring connectivity changes using {@link ConnectivityManager}. */
|
||||
public class ConnectivityMonitorWithConnectivityManager implements ConnectivityMonitor {
|
||||
private static final String TAG = "ConnMntrWConnMgr";
|
||||
private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
|
||||
|
||||
private final Listener listener;
|
||||
private final ConnectivityManager.NetworkCallback networkCallback;
|
||||
private final ConnectivityManager connectivityManager;
|
||||
// TODO(b/71901993): Ideally we shouldn't need this flag. However we still don't have clues why
|
||||
// the receiver is unregistered twice yet.
|
||||
private boolean isCallbackRegistered = false;
|
||||
|
||||
@SuppressWarnings({"nullness:assignment", "nullness:method.invocation"})
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public ConnectivityMonitorWithConnectivityManager(Context context, Listener listener) {
|
||||
this.listener = listener;
|
||||
|
||||
connectivityManager =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
networkCallback =
|
||||
new ConnectivityManager.NetworkCallback() {
|
||||
@Override
|
||||
public void onAvailable(Network network) {
|
||||
LOGGER.log("network available.");
|
||||
notifyConnectivityChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLost(Network network) {
|
||||
LOGGER.log("network lost.");
|
||||
notifyConnectivityChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnavailable() {
|
||||
LOGGER.log("network unavailable.");
|
||||
notifyConnectivityChange();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyConnectivityChange() {
|
||||
listener.onConnectivityChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts monitoring changes of connectivity of this device, which may indicate that the list of
|
||||
* network interfaces available for multi-cast messaging has changed.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void startWatchingConnectivityChanges() {
|
||||
LOGGER.log("Start watching connectivity changes");
|
||||
if (isCallbackRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
connectivityManager.registerNetworkCallback(
|
||||
new NetworkRequest.Builder().addTransportType(
|
||||
NetworkCapabilities.TRANSPORT_WIFI).build(),
|
||||
networkCallback);
|
||||
isCallbackRegistered = true;
|
||||
}
|
||||
|
||||
/** Stops monitoring changes of connectivity. */
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void stopWatchingConnectivityChanges() {
|
||||
LOGGER.log("Stop watching connectivity changes");
|
||||
if (!isCallbackRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback);
|
||||
isCallbackRegistered = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.android.server.connectivity.mdns.util.MdnsLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* A {@link Callable} that builds and enqueues a mDNS query to send over the multicast socket. If a
|
||||
* query is built and enqueued successfully, then call to {@link #call()} returns the transaction ID
|
||||
* and the list of the subtypes in the query as a {@link Pair}. If a query is failed to build, or if
|
||||
* it can not be enqueued, then call to {@link #call()} returns {@code null}.
|
||||
*/
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<String>>> {
|
||||
|
||||
private static final String TAG = "MdnsQueryCallable";
|
||||
private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
|
||||
private static final List<Integer> castShellEmulatorMdnsPorts;
|
||||
|
||||
static {
|
||||
castShellEmulatorMdnsPorts = new ArrayList<>();
|
||||
String[] stringPorts = MdnsConfigs.castShellEmulatorMdnsPorts();
|
||||
|
||||
for (String port : stringPorts) {
|
||||
try {
|
||||
castShellEmulatorMdnsPorts.add(Integer.parseInt(port));
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final WeakReference<MdnsSocketClient> weakRequestSender;
|
||||
private final MdnsPacketWriter packetWriter;
|
||||
private final String[] serviceTypeLabels;
|
||||
private final List<String> subtypes;
|
||||
private final boolean expectUnicastResponse;
|
||||
private final int transactionId;
|
||||
|
||||
EnqueueMdnsQueryCallable(
|
||||
@NonNull MdnsSocketClient requestSender,
|
||||
@NonNull MdnsPacketWriter packetWriter,
|
||||
@NonNull String serviceType,
|
||||
@NonNull Collection<String> subtypes,
|
||||
boolean expectUnicastResponse,
|
||||
int transactionId) {
|
||||
weakRequestSender = new WeakReference<>(requestSender);
|
||||
this.packetWriter = packetWriter;
|
||||
serviceTypeLabels = TextUtils.split(serviceType, "\\.");
|
||||
this.subtypes = new ArrayList<>(subtypes);
|
||||
this.expectUnicastResponse = expectUnicastResponse;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, List<String>> call() {
|
||||
try {
|
||||
MdnsSocketClient requestSender = weakRequestSender.get();
|
||||
if (requestSender == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int numQuestions = 1;
|
||||
if (!subtypes.isEmpty()) {
|
||||
numQuestions += subtypes.size();
|
||||
}
|
||||
|
||||
// Header.
|
||||
packetWriter.writeUInt16(transactionId); // transaction ID
|
||||
packetWriter.writeUInt16(MdnsConstants.FLAGS_QUERY); // flags
|
||||
packetWriter.writeUInt16(numQuestions); // number of questions
|
||||
packetWriter.writeUInt16(0); // number of answers (not yet known; will be written later)
|
||||
packetWriter.writeUInt16(0); // number of authority entries
|
||||
packetWriter.writeUInt16(0); // number of additional records
|
||||
|
||||
// Question(s). There will be one question for each (fqdn+subtype, recordType)
|
||||
// combination,
|
||||
// as well as one for each (fqdn, recordType) combination.
|
||||
|
||||
for (String subtype : subtypes) {
|
||||
String[] labels = new String[serviceTypeLabels.length + 2];
|
||||
labels[0] = MdnsConstants.SUBTYPE_PREFIX + subtype;
|
||||
labels[1] = MdnsConstants.SUBTYPE_LABEL;
|
||||
System.arraycopy(serviceTypeLabels, 0, labels, 2, serviceTypeLabels.length);
|
||||
|
||||
packetWriter.writeLabels(labels);
|
||||
packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
|
||||
packetWriter.writeUInt16(
|
||||
MdnsConstants.QCLASS_INTERNET
|
||||
| (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
|
||||
}
|
||||
|
||||
packetWriter.writeLabels(serviceTypeLabels);
|
||||
packetWriter.writeUInt16(MdnsRecord.TYPE_PTR);
|
||||
packetWriter.writeUInt16(
|
||||
MdnsConstants.QCLASS_INTERNET
|
||||
| (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
|
||||
|
||||
InetAddress mdnsAddress = MdnsConstants.getMdnsIPv4Address();
|
||||
if (requestSender.isOnIPv6OnlyNetwork()) {
|
||||
mdnsAddress = MdnsConstants.getMdnsIPv6Address();
|
||||
}
|
||||
|
||||
sendPacketTo(requestSender,
|
||||
new InetSocketAddress(mdnsAddress, MdnsConstants.MDNS_PORT));
|
||||
for (Integer emulatorPort : castShellEmulatorMdnsPorts) {
|
||||
sendPacketTo(requestSender, new InetSocketAddress(mdnsAddress, emulatorPort));
|
||||
}
|
||||
return Pair.create(transactionId, subtypes);
|
||||
} catch (IOException e) {
|
||||
LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
|
||||
TextUtils.join(",", subtypes)), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
|
||||
throws IOException {
|
||||
DatagramPacket packet = packetWriter.getPacket(address);
|
||||
if (expectUnicastResponse) {
|
||||
requestSender.sendUnicastPacket(packet);
|
||||
} else {
|
||||
requestSender.sendMulticastPacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.util.ArraySet;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* This class provides {@link ScheduledExecutorService} instances to {@link MdnsServiceTypeClient}
|
||||
* instances, and provides method to shutdown all the created executors.
|
||||
*/
|
||||
public class ExecutorProvider {
|
||||
|
||||
private final Set<ScheduledExecutorService> serviceTypeClientSchedulerExecutors =
|
||||
new ArraySet<>();
|
||||
|
||||
/** Returns a new {@link ScheduledExecutorService} instance. */
|
||||
public ScheduledExecutorService newServiceTypeClientSchedulerExecutor() {
|
||||
// TODO: actually use a pool ?
|
||||
ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1);
|
||||
serviceTypeClientSchedulerExecutors.add(executor);
|
||||
return executor;
|
||||
}
|
||||
|
||||
/** Shuts down all the created {@link ScheduledExecutorService} instances. */
|
||||
public void shutdownAll() {
|
||||
for (ScheduledExecutorService executor : serviceTypeClientSchedulerExecutors) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
/**
|
||||
* mDNS configuration values.
|
||||
*
|
||||
* TODO: consider making some of these adjustable via flags.
|
||||
*/
|
||||
public class MdnsConfigs {
|
||||
public static String[] castShellEmulatorMdnsPorts() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
public static long initialTimeBetweenBurstsMs() {
|
||||
return 5000L;
|
||||
}
|
||||
|
||||
public static long timeBetweenBurstsMs() {
|
||||
return 20_000L;
|
||||
}
|
||||
|
||||
public static int queriesPerBurst() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
public static long timeBetweenQueriesInBurstMs() {
|
||||
return 1000L;
|
||||
}
|
||||
|
||||
public static int queriesPerBurstPassive() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public static boolean alwaysAskForUnicastResponseInEachBurst() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean useSessionIdToScheduleMdnsTask() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean shouldCancelScanTaskWhenFutureIsNull() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static long sleepTimeForSocketThreadMs() {
|
||||
return 20_000L;
|
||||
}
|
||||
|
||||
public static boolean checkMulticastResponse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean useSeparateSocketToSendUnicastQuery() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static long checkMulticastResponseIntervalMs() {
|
||||
return 10_000L;
|
||||
}
|
||||
|
||||
public static boolean clearMdnsPacketQueueAfterDiscoveryStops() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean allowAddMdnsPacketAfterDiscoveryStops() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int mdnsPacketQueueMaxSize() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
public static boolean preferIpv6() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** mDNS-related constants. */
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
@VisibleForTesting
|
||||
public final class MdnsConstants {
|
||||
public static final int MDNS_PORT = 5353;
|
||||
// Flags word format is:
|
||||
// 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
|
||||
// QR [ Opcode ] AA TC RD RA Z AD CD [ Rcode ]
|
||||
// See http://www.networksorcery.com/enp/protocol/dns.htm
|
||||
// For responses, QR bit should be 1, AA - CD bits should be ignored, and all other bits
|
||||
// should be 0.
|
||||
public static final int FLAGS_QUERY = 0x0000;
|
||||
public static final int FLAGS_RESPONSE_MASK = 0xF80F;
|
||||
public static final int FLAGS_RESPONSE = 0x8000;
|
||||
public static final int QCLASS_INTERNET = 0x0001;
|
||||
public static final int QCLASS_UNICAST = 0x8000;
|
||||
public static final String SUBTYPE_LABEL = "_sub";
|
||||
public static final String SUBTYPE_PREFIX = "_";
|
||||
private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
|
||||
private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
|
||||
private static InetAddress mdnsAddress;
|
||||
private static Charset utf8Charset;
|
||||
private MdnsConstants() {
|
||||
}
|
||||
|
||||
public static InetAddress getMdnsIPv4Address() {
|
||||
synchronized (MdnsConstants.class) {
|
||||
InetAddress addr = null;
|
||||
try {
|
||||
addr = InetAddress.getByName(MDNS_IPV4_HOST_ADDRESS);
|
||||
} catch (UnknownHostException e) {
|
||||
/* won't happen */
|
||||
}
|
||||
mdnsAddress = addr;
|
||||
return mdnsAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public static InetAddress getMdnsIPv6Address() {
|
||||
synchronized (MdnsConstants.class) {
|
||||
InetAddress addr = null;
|
||||
try {
|
||||
addr = InetAddress.getByName(MDNS_IPV6_HOST_ADDRESS);
|
||||
} catch (UnknownHostException e) {
|
||||
/* won't happen */
|
||||
}
|
||||
mdnsAddress = addr;
|
||||
return mdnsAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public static Charset getUtf8Charset() {
|
||||
synchronized (MdnsConstants.class) {
|
||||
if (utf8Charset == null) {
|
||||
utf8Charset = getUtf8CharsetOnKitKat();
|
||||
}
|
||||
return utf8Charset;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
private static Charset getUtf8CharsetOnKitKat() {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.RequiresPermission;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.connectivity.mdns.util.MdnsLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
|
||||
* notify them when a mDNS service instance is found, updated, or removed?
|
||||
*/
|
||||
public class MdnsDiscoveryManager implements MdnsSocketClient.Callback {
|
||||
|
||||
private static final MdnsLogger LOGGER = new MdnsLogger("MdnsDiscoveryManager");
|
||||
|
||||
private final ExecutorProvider executorProvider;
|
||||
private final MdnsSocketClient socketClient;
|
||||
|
||||
private final Map<String, MdnsServiceTypeClient> serviceTypeClients = new ArrayMap<>();
|
||||
|
||||
public MdnsDiscoveryManager(
|
||||
@NonNull ExecutorProvider executorProvider, @NonNull MdnsSocketClient socketClient) {
|
||||
this.executorProvider = executorProvider;
|
||||
this.socketClient = socketClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
|
||||
* {@code listener} for receiving mDNS service discovery responses.
|
||||
*
|
||||
* @param serviceType The type of the service to discover.
|
||||
* @param listener The {@link MdnsServiceBrowserListener} listener.
|
||||
* @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
|
||||
* serviceType}.
|
||||
*/
|
||||
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
|
||||
public synchronized void registerListener(
|
||||
@NonNull String serviceType,
|
||||
@NonNull MdnsServiceBrowserListener listener,
|
||||
@NonNull MdnsSearchOptions searchOptions) {
|
||||
LOGGER.log(
|
||||
"Registering listener for subtypes: %s",
|
||||
TextUtils.join(",", searchOptions.getSubtypes()));
|
||||
if (serviceTypeClients.isEmpty()) {
|
||||
// First listener. Starts the socket client.
|
||||
try {
|
||||
socketClient.startDiscovery();
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to start discover.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// All listeners of the same service types shares the same MdnsServiceTypeClient.
|
||||
MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
|
||||
if (serviceTypeClient == null) {
|
||||
serviceTypeClient = createServiceTypeClient(serviceType);
|
||||
serviceTypeClients.put(serviceType, serviceTypeClient);
|
||||
}
|
||||
serviceTypeClient.startSendAndReceive(listener, searchOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
|
||||
* registered for the given service type, stops discovery for the service type.
|
||||
*
|
||||
* @param serviceType The type of the service to discover.
|
||||
* @param listener The {@link MdnsServiceBrowserListener} listener.
|
||||
*/
|
||||
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
|
||||
public synchronized void unregisterListener(
|
||||
@NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
|
||||
LOGGER.log("Unregistering listener for service type: %s", serviceType);
|
||||
MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(serviceType);
|
||||
if (serviceTypeClient == null) {
|
||||
return;
|
||||
}
|
||||
if (serviceTypeClient.stopSendAndReceive(listener)) {
|
||||
// No listener is registered for the service type anymore, remove it from the list of
|
||||
// the
|
||||
// service type clients.
|
||||
serviceTypeClients.remove(serviceType);
|
||||
if (serviceTypeClients.isEmpty()) {
|
||||
// No discovery request. Stops the socket client.
|
||||
socketClient.stopDiscovery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onResponseReceived(@NonNull MdnsResponse response) {
|
||||
String[] name =
|
||||
response.getPointerRecords().isEmpty()
|
||||
? null
|
||||
: response.getPointerRecords().get(0).getName();
|
||||
if (name != null) {
|
||||
for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
|
||||
String[] serviceType = serviceTypeClient.getServiceTypeLabels();
|
||||
if ((Arrays.equals(name, serviceType)
|
||||
|| ((name.length == (serviceType.length + 2))
|
||||
&& name[1].equals(MdnsConstants.SUBTYPE_LABEL)
|
||||
&& MdnsRecord.labelsAreSuffix(serviceType, name)))) {
|
||||
serviceTypeClient.processResponse(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode) {
|
||||
for (MdnsServiceTypeClient serviceTypeClient : serviceTypeClients.values()) {
|
||||
serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType) {
|
||||
return new MdnsServiceTypeClient(
|
||||
serviceType, socketClient,
|
||||
executorProvider.newServiceTypeClientSchedulerExecutor());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
@VisibleForTesting
|
||||
public class MdnsInetAddressRecord extends MdnsRecord {
|
||||
private Inet6Address inet6Address;
|
||||
private Inet4Address inet4Address;
|
||||
|
||||
/**
|
||||
* Constructs the {@link MdnsRecord}
|
||||
*
|
||||
* @param name the service host name
|
||||
* @param type the type of record (either Type 'AAAA' or Type 'A')
|
||||
* @param reader the reader to read the record from.
|
||||
*/
|
||||
public MdnsInetAddressRecord(String[] name, int type, MdnsPacketReader reader)
|
||||
throws IOException {
|
||||
super(name, type, reader);
|
||||
}
|
||||
|
||||
/** Returns the IPv6 address. */
|
||||
public Inet6Address getInet6Address() {
|
||||
return inet6Address;
|
||||
}
|
||||
|
||||
/** Returns the IPv4 address. */
|
||||
public Inet4Address getInet4Address() {
|
||||
return inet4Address;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readData(MdnsPacketReader reader) throws IOException {
|
||||
int size = 4;
|
||||
if (super.getType() == MdnsRecord.TYPE_AAAA) {
|
||||
size = 16;
|
||||
}
|
||||
byte[] buf = new byte[size];
|
||||
reader.readBytes(buf);
|
||||
try {
|
||||
InetAddress address = InetAddress.getByAddress(buf);
|
||||
if (address instanceof Inet4Address) {
|
||||
inet4Address = (Inet4Address) address;
|
||||
inet6Address = null;
|
||||
} else if (address instanceof Inet6Address) {
|
||||
inet4Address = null;
|
||||
inet6Address = (Inet6Address) address;
|
||||
} else {
|
||||
inet4Address = null;
|
||||
inet6Address = null;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
// Ignore exception
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeData(MdnsPacketWriter writer) throws IOException {
|
||||
byte[] buf = null;
|
||||
if (inet4Address != null) {
|
||||
buf = inet4Address.getAddress();
|
||||
} else if (inet6Address != null) {
|
||||
buf = inet6Address.getAddress();
|
||||
}
|
||||
if (buf != null) {
|
||||
writer.writeBytes(buf);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String type = "AAAA";
|
||||
if (super.getType() == MdnsRecord.TYPE_A) {
|
||||
type = "A";
|
||||
}
|
||||
return String.format(
|
||||
Locale.ROOT, "%s: Inet4Address: %s Inet6Address: %s", type, inet4Address,
|
||||
inet6Address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (super.hashCode() * 31)
|
||||
+ Objects.hashCode(inet4Address)
|
||||
+ Objects.hashCode(inet6Address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof MdnsInetAddressRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.equals(other)
|
||||
&& Objects.equals(inet4Address, ((MdnsInetAddressRecord) other).inet4Address)
|
||||
&& Objects.equals(inet6Address, ((MdnsInetAddressRecord) other).inet6Address);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Simple decoder for mDNS packets. */
|
||||
public class MdnsPacketReader {
|
||||
private final byte[] buf;
|
||||
private final int count;
|
||||
private final SparseArray<LabelEntry> labelDictionary;
|
||||
private int pos;
|
||||
private int limit;
|
||||
|
||||
/** Constructs a reader for the given packet. */
|
||||
public MdnsPacketReader(DatagramPacket packet) {
|
||||
buf = packet.getData();
|
||||
count = packet.getLength();
|
||||
pos = 0;
|
||||
limit = -1;
|
||||
labelDictionary = new SparseArray<>(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a temporary limit (from the current read position) for subsequent reads. Any attempt to
|
||||
* read past this limit will result in an EOFException.
|
||||
*
|
||||
* @param limit The new limit.
|
||||
* @throws IOException If there is insufficient data for the new limit.
|
||||
*/
|
||||
public void setLimit(int limit) throws IOException {
|
||||
if (limit >= 0) {
|
||||
if (pos + limit <= count) {
|
||||
this.limit = pos + limit;
|
||||
} else {
|
||||
throw new IOException(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"attempt to set limit beyond available data: %d exceeds %d",
|
||||
pos + limit,
|
||||
count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears the limit set by {@link #setLimit}. */
|
||||
public void clearLimit() {
|
||||
limit = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes left to read, between the current read position and either the
|
||||
* limit (if set) or the end of the packet.
|
||||
*/
|
||||
public int getRemaining() {
|
||||
return (limit >= 0 ? limit : count) - pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned 8-bit integer.
|
||||
*
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public int readUInt8() throws EOFException {
|
||||
checkRemaining(1);
|
||||
byte val = buf[pos++];
|
||||
return val & 0xFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned 16-bit integer.
|
||||
*
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public int readUInt16() throws EOFException {
|
||||
checkRemaining(2);
|
||||
int val = (buf[pos++] & 0xFF) << 8;
|
||||
val |= (buf[pos++]) & 0xFF;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned 32-bit integer.
|
||||
*
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public long readUInt32() throws EOFException {
|
||||
checkRemaining(4);
|
||||
long val = (long) (buf[pos++] & 0xFF) << 24;
|
||||
val |= (long) (buf[pos++] & 0xFF) << 16;
|
||||
val |= (long) (buf[pos++] & 0xFF) << 8;
|
||||
val |= buf[pos++] & 0xFF;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a sequence of labels and returns them as an array of strings. A sequence of labels is
|
||||
* either a sequence of strings terminated by a NUL byte, a sequence of strings terminated by a
|
||||
* pointer, or a pointer.
|
||||
*
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
* @throws IOException If invalid data is read.
|
||||
*/
|
||||
public String[] readLabels() throws IOException {
|
||||
List<String> result = new ArrayList<>(5);
|
||||
LabelEntry previousEntry = null;
|
||||
|
||||
while (getRemaining() > 0) {
|
||||
byte nextByte = peekByte();
|
||||
|
||||
if (nextByte == 0) {
|
||||
// A NUL byte terminates a sequence of labels.
|
||||
skip(1);
|
||||
break;
|
||||
}
|
||||
|
||||
int currentOffset = pos;
|
||||
|
||||
boolean isLabelPointer = (nextByte & 0xC0) == 0xC0;
|
||||
if (isLabelPointer) {
|
||||
// A pointer terminates a sequence of labels. Store the pointer value in the
|
||||
// previous label entry.
|
||||
int labelOffset = ((readUInt8() & 0x3F) << 8) | (readUInt8() & 0xFF);
|
||||
if (previousEntry != null) {
|
||||
previousEntry.nextOffset = labelOffset;
|
||||
}
|
||||
|
||||
// Follow the chain of labels starting at this pointer, adding all of them onto the
|
||||
// result.
|
||||
while (labelOffset != 0) {
|
||||
LabelEntry entry = labelDictionary.get(labelOffset);
|
||||
if (entry == null) {
|
||||
throw new IOException(
|
||||
String.format(Locale.ROOT, "Invalid label pointer: %04X",
|
||||
labelOffset));
|
||||
}
|
||||
result.add(entry.label);
|
||||
labelOffset = entry.nextOffset;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
// It's an ordinary label. Chain it onto the previous label entry (if any), and add
|
||||
// it onto the result.
|
||||
String val = readString();
|
||||
LabelEntry newEntry = new LabelEntry(val);
|
||||
labelDictionary.put(currentOffset, newEntry);
|
||||
|
||||
if (previousEntry != null) {
|
||||
previousEntry.nextOffset = currentOffset;
|
||||
}
|
||||
previousEntry = newEntry;
|
||||
result.add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a length-prefixed string.
|
||||
*
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public String readString() throws EOFException {
|
||||
int len = readUInt8();
|
||||
checkRemaining(len);
|
||||
String val = new String(buf, pos, len, MdnsConstants.getUtf8Charset());
|
||||
pos += len;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a specific number of bytes.
|
||||
*
|
||||
* @param bytes The array to fill.
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public void readBytes(byte[] bytes) throws EOFException {
|
||||
checkRemaining(bytes.length);
|
||||
System.arraycopy(buf, pos, bytes, 0, bytes.length);
|
||||
pos += bytes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips over the given number of bytes.
|
||||
*
|
||||
* @param count The number of bytes to read and discard.
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public void skip(int count) throws EOFException {
|
||||
checkRemaining(count);
|
||||
pos += count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peeks at and returns the next byte in the packet, without advancing the read position.
|
||||
*
|
||||
* @throws EOFException If there are not enough bytes remaining in the packet to satisfy the
|
||||
* read.
|
||||
*/
|
||||
public byte peekByte() throws EOFException {
|
||||
checkRemaining(1);
|
||||
return buf[pos];
|
||||
}
|
||||
|
||||
/** Returns the current byte position of the reader for the data packet. */
|
||||
public int getPosition() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Checks if the number of remaining bytes to be read in the packet is at least |count|.
|
||||
private void checkRemaining(int count) throws EOFException {
|
||||
if (getRemaining() < count) {
|
||||
throw new EOFException();
|
||||
}
|
||||
}
|
||||
|
||||
private static class LabelEntry {
|
||||
public final String label;
|
||||
public int nextOffset = 0;
|
||||
|
||||
public LabelEntry(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Simple encoder for mDNS packets. */
|
||||
public class MdnsPacketWriter {
|
||||
private static final int MDNS_POINTER_MASK = 0xC000;
|
||||
private final byte[] data;
|
||||
private final Map<Integer, String[]> labelDictionary;
|
||||
private int pos = 0;
|
||||
private int savedWritePos = -1;
|
||||
|
||||
/**
|
||||
* Constructs a writer for a new packet.
|
||||
*
|
||||
* @param maxSize The maximum size of a packet.
|
||||
*/
|
||||
public MdnsPacketWriter(int maxSize) {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("invalid size");
|
||||
}
|
||||
|
||||
data = new byte[maxSize];
|
||||
labelDictionary = new HashMap<>();
|
||||
}
|
||||
|
||||
/** Returns the current write position. */
|
||||
public int getWritePosition() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current write position and then rewinds the write position by the given number of
|
||||
* bytes. This is useful for updating length fields earlier in the packet. Rewinds cannot be
|
||||
* nested.
|
||||
*
|
||||
* @param position The position to rewind to.
|
||||
* @throws IOException If the count would go beyond the beginning of the packet, or if there is
|
||||
* already a rewind in effect.
|
||||
*/
|
||||
public void rewind(int position) throws IOException {
|
||||
if ((savedWritePos != -1) || (position > pos) || (position < 0)) {
|
||||
throw new IOException("invalid rewind");
|
||||
}
|
||||
|
||||
savedWritePos = pos;
|
||||
pos = position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current write position to what it was prior to the last rewind.
|
||||
*
|
||||
* @throws IOException If there was no rewind in effect.
|
||||
*/
|
||||
public void unrewind() throws IOException {
|
||||
if (savedWritePos == -1) {
|
||||
throw new IOException("no rewind is in effect");
|
||||
}
|
||||
pos = savedWritePos;
|
||||
savedWritePos = -1;
|
||||
}
|
||||
|
||||
/** Clears any rewind state. */
|
||||
public void clearRewind() {
|
||||
savedWritePos = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an unsigned 8-bit integer.
|
||||
*
|
||||
* @param value The value to write.
|
||||
* @throws IOException If there is not enough space remaining in the packet.
|
||||
*/
|
||||
public void writeUInt8(int value) throws IOException {
|
||||
checkRemaining(1);
|
||||
data[pos++] = (byte) (value & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an unsigned 16-bit integer.
|
||||
*
|
||||
* @param value The value to write.
|
||||
* @throws IOException If there is not enough space remaining in the packet.
|
||||
*/
|
||||
public void writeUInt16(int value) throws IOException {
|
||||
checkRemaining(2);
|
||||
data[pos++] = (byte) ((value >>> 8) & 0xFF);
|
||||
data[pos++] = (byte) (value & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an unsigned 32-bit integer.
|
||||
*
|
||||
* @param value The value to write.
|
||||
* @throws IOException If there is not enough space remaining in the packet.
|
||||
*/
|
||||
public void writeUInt32(long value) throws IOException {
|
||||
checkRemaining(4);
|
||||
data[pos++] = (byte) ((value >>> 24) & 0xFF);
|
||||
data[pos++] = (byte) ((value >>> 16) & 0xFF);
|
||||
data[pos++] = (byte) ((value >>> 8) & 0xFF);
|
||||
data[pos++] = (byte) (value & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a specific number of bytes.
|
||||
*
|
||||
* @param data The array to write.
|
||||
* @throws IOException If there is not enough space remaining in the packet.
|
||||
*/
|
||||
public void writeBytes(byte[] data) throws IOException {
|
||||
checkRemaining(data.length);
|
||||
System.arraycopy(data, 0, this.data, pos, data.length);
|
||||
pos += data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string.
|
||||
*
|
||||
* @param value The string to write.
|
||||
* @throws IOException If there is not enough space remaining in the packet.
|
||||
*/
|
||||
public void writeString(String value) throws IOException {
|
||||
byte[] utf8 = value.getBytes(MdnsConstants.getUtf8Charset());
|
||||
writeUInt8(utf8.length);
|
||||
writeBytes(utf8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a series of labels. Uses name compression.
|
||||
*
|
||||
* @param labels The labels to write.
|
||||
* @throws IOException If there is not enough space remaining in the packet.
|
||||
*/
|
||||
public void writeLabels(String[] labels) throws IOException {
|
||||
// See section 4.1.4 of RFC 1035 (http://tools.ietf.org/html/rfc1035) for a description
|
||||
// of the name compression method used here.
|
||||
|
||||
int suffixLength = 0;
|
||||
int suffixPointer = 0;
|
||||
|
||||
for (Map.Entry<Integer, String[]> entry : labelDictionary.entrySet()) {
|
||||
int existingOffset = entry.getKey();
|
||||
String[] existingLabels = entry.getValue();
|
||||
|
||||
if (Arrays.equals(existingLabels, labels)) {
|
||||
writePointer(existingOffset);
|
||||
return;
|
||||
} else if (MdnsRecord.labelsAreSuffix(existingLabels, labels)) {
|
||||
// Keep track of the longest matching suffix so far.
|
||||
if (existingLabels.length > suffixLength) {
|
||||
suffixLength = existingLabels.length;
|
||||
suffixPointer = existingOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suffixLength > 0) {
|
||||
for (int i = 0; i < (labels.length - suffixLength); ++i) {
|
||||
writeString(labels[i]);
|
||||
}
|
||||
writePointer(suffixPointer);
|
||||
} else {
|
||||
int[] offsets = new int[labels.length];
|
||||
for (int i = 0; i < labels.length; ++i) {
|
||||
offsets[i] = getWritePosition();
|
||||
writeString(labels[i]);
|
||||
}
|
||||
writeUInt8(0); // NUL terminator
|
||||
|
||||
// Add entries to the label dictionary for each suffix of the label list, including
|
||||
// the whole list itself.
|
||||
for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
|
||||
String[] value = new String[len];
|
||||
System.arraycopy(labels, i, value, 0, len);
|
||||
labelDictionary.put(offsets[i], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the number of bytes that can still be written. */
|
||||
public int getRemaining() {
|
||||
return data.length - pos;
|
||||
}
|
||||
|
||||
// Writes a pointer to a label.
|
||||
private void writePointer(int offset) throws IOException {
|
||||
writeUInt16(MDNS_POINTER_MASK | offset);
|
||||
}
|
||||
|
||||
// Checks if the remaining space in the packet is at least |count|.
|
||||
private void checkRemaining(int count) throws IOException {
|
||||
if (getRemaining() < count) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds and returns the packet. */
|
||||
public DatagramPacket getPacket(SocketAddress destAddress) throws IOException {
|
||||
return new DatagramPacket(data, pos, destAddress);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** An mDNS "PTR" record, which holds a name (the "pointer"). */
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
@VisibleForTesting
|
||||
public class MdnsPointerRecord extends MdnsRecord {
|
||||
private String[] pointer;
|
||||
|
||||
public MdnsPointerRecord(String[] name, MdnsPacketReader reader) throws IOException {
|
||||
super(name, TYPE_PTR, reader);
|
||||
}
|
||||
|
||||
/** Returns the pointer as an array of labels. */
|
||||
public String[] getPointer() {
|
||||
return pointer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readData(MdnsPacketReader reader) throws IOException {
|
||||
pointer = reader.readLabels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeData(MdnsPacketWriter writer) throws IOException {
|
||||
writer.writeLabels(pointer);
|
||||
}
|
||||
|
||||
public boolean hasSubtype() {
|
||||
return (name != null) && (name.length > 2) && name[1].equals(MdnsConstants.SUBTYPE_LABEL);
|
||||
}
|
||||
|
||||
public String getSubtype() {
|
||||
return hasSubtype() ? name[0] : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PTR: " + labelsToString(name) + " -> " + labelsToString(pointer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (super.hashCode() * 31) + Arrays.hashCode(pointer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof MdnsPointerRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.equals(other) && Arrays.equals(pointer, ((MdnsPointerRecord) other).pointer);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Abstract base class for mDNS records. Stores the header fields and provides methods for reading
|
||||
* the record from and writing it to a packet.
|
||||
*/
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
public abstract class MdnsRecord {
|
||||
public static final int TYPE_A = 0x0001;
|
||||
public static final int TYPE_AAAA = 0x001C;
|
||||
public static final int TYPE_PTR = 0x000C;
|
||||
public static final int TYPE_SRV = 0x0021;
|
||||
public static final int TYPE_TXT = 0x0010;
|
||||
|
||||
/** Status indicating that the record is current. */
|
||||
public static final int STATUS_OK = 0;
|
||||
/** Status indicating that the record has expired (TTL reached 0). */
|
||||
public static final int STATUS_EXPIRED = 1;
|
||||
/** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
|
||||
public static final int STATUS_NEEDS_REFRESH = 2;
|
||||
|
||||
protected final String[] name;
|
||||
private final int type;
|
||||
private final int cls;
|
||||
private final long receiptTimeMillis;
|
||||
private final long ttlMillis;
|
||||
private Object key;
|
||||
|
||||
/**
|
||||
* Constructs a new record with the given name and type.
|
||||
*
|
||||
* @param reader The reader to read the record from.
|
||||
* @throws IOException If an error occurs while reading the packet.
|
||||
*/
|
||||
protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
cls = reader.readUInt16();
|
||||
ttlMillis = TimeUnit.SECONDS.toMillis(reader.readUInt32());
|
||||
int dataLength = reader.readUInt16();
|
||||
|
||||
receiptTimeMillis = SystemClock.elapsedRealtime();
|
||||
|
||||
reader.setLimit(dataLength);
|
||||
readData(reader);
|
||||
reader.clearLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of labels into their dot-separated string representation. This method
|
||||
* should
|
||||
* be used for logging purposes only.
|
||||
*/
|
||||
public static String labelsToString(String[] labels) {
|
||||
if (labels == null) {
|
||||
return null;
|
||||
}
|
||||
return TextUtils.join(".", labels);
|
||||
}
|
||||
|
||||
/** Tests if |list1| is a suffix of |list2|. */
|
||||
public static boolean labelsAreSuffix(String[] list1, String[] list2) {
|
||||
int offset = list2.length - list1.length;
|
||||
|
||||
if (offset < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < list1.length; ++i) {
|
||||
if (!list1[i].equals(list2[i + offset])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns the record's receipt (creation) time. */
|
||||
public final long getReceiptTime() {
|
||||
return receiptTimeMillis;
|
||||
}
|
||||
|
||||
/** Returns the record's name. */
|
||||
public String[] getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Returns the record's original TTL, in milliseconds. */
|
||||
public final long getTtl() {
|
||||
return ttlMillis;
|
||||
}
|
||||
|
||||
/** Returns the record's type. */
|
||||
public final int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the record's remaining TTL.
|
||||
*
|
||||
* @param now The current system time.
|
||||
* @return The remaning TTL, in milliseconds.
|
||||
*/
|
||||
public long getRemainingTTL(final long now) {
|
||||
long age = now - receiptTimeMillis;
|
||||
if (age > ttlMillis) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ttlMillis - age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the record's payload from a packet.
|
||||
*
|
||||
* @param reader The reader to use.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
protected abstract void readData(MdnsPacketReader reader) throws IOException;
|
||||
|
||||
/**
|
||||
* Writes the record to a packet.
|
||||
*
|
||||
* @param writer The writer to use.
|
||||
* @param now The current system time. This is used when writing the updated TTL.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public final void write(MdnsPacketWriter writer, long now) throws IOException {
|
||||
writer.writeLabels(name);
|
||||
writer.writeUInt16(type);
|
||||
writer.writeUInt16(cls);
|
||||
|
||||
writer.writeUInt32(TimeUnit.MILLISECONDS.toSeconds(getRemainingTTL(now)));
|
||||
|
||||
int dataLengthPos = writer.getWritePosition();
|
||||
writer.writeUInt16(0); // data length
|
||||
int dataPos = writer.getWritePosition();
|
||||
|
||||
writeData(writer);
|
||||
|
||||
// Calculate amount of data written, and overwrite the data field earlier in the packet.
|
||||
int endPos = writer.getWritePosition();
|
||||
int dataLength = endPos - dataPos;
|
||||
writer.rewind(dataLengthPos);
|
||||
writer.writeUInt16(dataLength);
|
||||
writer.unrewind();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the record's payload to a packet.
|
||||
*
|
||||
* @param writer The writer to use.
|
||||
* @throws IOException If an I/O error occurs.
|
||||
*/
|
||||
protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
|
||||
|
||||
/** Gets the status of the record. */
|
||||
public int getStatus(final long now) {
|
||||
final long age = now - receiptTimeMillis;
|
||||
if (age > ttlMillis) {
|
||||
return STATUS_EXPIRED;
|
||||
}
|
||||
if (age > (ttlMillis / 2)) {
|
||||
return STATUS_NEEDS_REFRESH;
|
||||
}
|
||||
return STATUS_OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof MdnsRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MdnsRecord otherRecord = (MdnsRecord) other;
|
||||
|
||||
return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(Arrays.hashCode(name), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an opaque object that uniquely identifies this record through a combination of its
|
||||
* type
|
||||
* and name. Suitable for use as a key in caches.
|
||||
*/
|
||||
public final Object getKey() {
|
||||
if (key == null) {
|
||||
key = new Key(type, name);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private static final class Key {
|
||||
private final int recordType;
|
||||
private final String[] recordName;
|
||||
|
||||
public Key(int recordType, String[] recordName) {
|
||||
this.recordType = recordType;
|
||||
this.recordName = recordName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof Key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Key otherKey = (Key) other;
|
||||
|
||||
return (recordType == otherKey.recordType) && Arrays.equals(recordName,
|
||||
otherKey.recordName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (recordType * 31) + Arrays.hashCode(recordName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/** An mDNS response. */
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
public class MdnsResponse {
|
||||
private final List<MdnsRecord> records;
|
||||
private final List<MdnsPointerRecord> pointerRecords;
|
||||
private MdnsServiceRecord serviceRecord;
|
||||
private MdnsTextRecord textRecord;
|
||||
private MdnsInetAddressRecord inet4AddressRecord;
|
||||
private MdnsInetAddressRecord inet6AddressRecord;
|
||||
private long lastUpdateTime;
|
||||
|
||||
/** Constructs a new, empty response. */
|
||||
public MdnsResponse(long now) {
|
||||
lastUpdateTime = now;
|
||||
records = new LinkedList<>();
|
||||
pointerRecords = new LinkedList<>();
|
||||
}
|
||||
|
||||
// This generic typed helper compares records for equality.
|
||||
// Returns True if records are the same.
|
||||
private <T> boolean recordsAreSame(T a, T b) {
|
||||
return ((a == null) && (b == null)) || ((a != null) && (b != null) && a.equals(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a pointer record.
|
||||
*
|
||||
* @return <code>true</code> if the record was added, or <code>false</code> if a matching
|
||||
* pointer
|
||||
* record is already present in the response.
|
||||
*/
|
||||
public synchronized boolean addPointerRecord(MdnsPointerRecord pointerRecord) {
|
||||
if (!pointerRecords.contains(pointerRecord)) {
|
||||
pointerRecords.add(pointerRecord);
|
||||
records.add(pointerRecord);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Gets the pointer records. */
|
||||
public synchronized List<MdnsPointerRecord> getPointerRecords() {
|
||||
// Returns a shallow copy.
|
||||
return new LinkedList<>(pointerRecords);
|
||||
}
|
||||
|
||||
public synchronized boolean hasPointerRecords() {
|
||||
return !pointerRecords.isEmpty();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ synchronized void clearPointerRecords() {
|
||||
pointerRecords.clear();
|
||||
}
|
||||
|
||||
public synchronized boolean hasSubtypes() {
|
||||
for (MdnsPointerRecord pointerRecord : pointerRecords) {
|
||||
if (pointerRecord.hasSubtype()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public synchronized List<String> getSubtypes() {
|
||||
List<String> subtypes = null;
|
||||
|
||||
for (MdnsPointerRecord pointerRecord : pointerRecords) {
|
||||
if (pointerRecord.hasSubtype()) {
|
||||
if (subtypes == null) {
|
||||
subtypes = new LinkedList<>();
|
||||
}
|
||||
subtypes.add(pointerRecord.getSubtype());
|
||||
}
|
||||
}
|
||||
|
||||
return subtypes;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public synchronized void removeSubtypes() {
|
||||
Iterator<MdnsPointerRecord> iter = pointerRecords.iterator();
|
||||
while (iter.hasNext()) {
|
||||
MdnsPointerRecord pointerRecord = iter.next();
|
||||
if (pointerRecord.hasSubtype()) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the service record. */
|
||||
public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
|
||||
if (recordsAreSame(this.serviceRecord, serviceRecord)) {
|
||||
return false;
|
||||
}
|
||||
if (this.serviceRecord != null) {
|
||||
records.remove(this.serviceRecord);
|
||||
}
|
||||
this.serviceRecord = serviceRecord;
|
||||
if (this.serviceRecord != null) {
|
||||
records.add(this.serviceRecord);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Gets the service record. */
|
||||
public synchronized MdnsServiceRecord getServiceRecord() {
|
||||
return serviceRecord;
|
||||
}
|
||||
|
||||
public synchronized boolean hasServiceRecord() {
|
||||
return serviceRecord != null;
|
||||
}
|
||||
|
||||
/** Sets the text record. */
|
||||
public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
|
||||
if (recordsAreSame(this.textRecord, textRecord)) {
|
||||
return false;
|
||||
}
|
||||
if (this.textRecord != null) {
|
||||
records.remove(this.textRecord);
|
||||
}
|
||||
this.textRecord = textRecord;
|
||||
if (this.textRecord != null) {
|
||||
records.add(this.textRecord);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Gets the text record. */
|
||||
public synchronized MdnsTextRecord getTextRecord() {
|
||||
return textRecord;
|
||||
}
|
||||
|
||||
public synchronized boolean hasTextRecord() {
|
||||
return textRecord != null;
|
||||
}
|
||||
|
||||
/** Sets the IPv4 address record. */
|
||||
public synchronized boolean setInet4AddressRecord(MdnsInetAddressRecord newInet4AddressRecord) {
|
||||
if (recordsAreSame(this.inet4AddressRecord, newInet4AddressRecord)) {
|
||||
return false;
|
||||
}
|
||||
if (this.inet4AddressRecord != null) {
|
||||
records.remove(this.inet4AddressRecord);
|
||||
}
|
||||
if (newInet4AddressRecord != null && newInet4AddressRecord.getInet4Address() != null) {
|
||||
this.inet4AddressRecord = newInet4AddressRecord;
|
||||
records.add(this.inet4AddressRecord);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Gets the IPv4 address record. */
|
||||
public synchronized MdnsInetAddressRecord getInet4AddressRecord() {
|
||||
return inet4AddressRecord;
|
||||
}
|
||||
|
||||
public synchronized boolean hasInet4AddressRecord() {
|
||||
return inet4AddressRecord != null;
|
||||
}
|
||||
|
||||
/** Sets the IPv6 address record. */
|
||||
public synchronized boolean setInet6AddressRecord(MdnsInetAddressRecord newInet6AddressRecord) {
|
||||
if (recordsAreSame(this.inet6AddressRecord, newInet6AddressRecord)) {
|
||||
return false;
|
||||
}
|
||||
if (this.inet6AddressRecord != null) {
|
||||
records.remove(this.inet6AddressRecord);
|
||||
}
|
||||
if (newInet6AddressRecord != null && newInet6AddressRecord.getInet6Address() != null) {
|
||||
this.inet6AddressRecord = newInet6AddressRecord;
|
||||
records.add(this.inet6AddressRecord);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/** Gets the IPv6 address record. */
|
||||
public synchronized MdnsInetAddressRecord getInet6AddressRecord() {
|
||||
return inet6AddressRecord;
|
||||
}
|
||||
|
||||
public synchronized boolean hasInet6AddressRecord() {
|
||||
return inet6AddressRecord != null;
|
||||
}
|
||||
|
||||
/** Gets all of the records. */
|
||||
public synchronized List<MdnsRecord> getRecords() {
|
||||
return new LinkedList<>(records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges any records that are present in another response into this one.
|
||||
*
|
||||
* @return <code>true</code> if any records were added or updated.
|
||||
*/
|
||||
public synchronized boolean mergeRecordsFrom(MdnsResponse other) {
|
||||
lastUpdateTime = other.lastUpdateTime;
|
||||
|
||||
boolean updated = false;
|
||||
|
||||
List<MdnsPointerRecord> pointerRecords = other.getPointerRecords();
|
||||
if (pointerRecords != null) {
|
||||
for (MdnsPointerRecord pointerRecord : pointerRecords) {
|
||||
if (addPointerRecord(pointerRecord)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MdnsServiceRecord serviceRecord = other.getServiceRecord();
|
||||
if (serviceRecord != null) {
|
||||
if (setServiceRecord(serviceRecord)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
MdnsTextRecord textRecord = other.getTextRecord();
|
||||
if (textRecord != null) {
|
||||
if (setTextRecord(textRecord)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
MdnsInetAddressRecord otherInet4AddressRecord = other.getInet4AddressRecord();
|
||||
if (otherInet4AddressRecord != null && otherInet4AddressRecord.getInet4Address() != null) {
|
||||
if (setInet4AddressRecord(otherInet4AddressRecord)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
MdnsInetAddressRecord otherInet6AddressRecord = other.getInet6AddressRecord();
|
||||
if (otherInet6AddressRecord != null && otherInet6AddressRecord.getInet6Address() != null) {
|
||||
if (setInet6AddressRecord(otherInet6AddressRecord)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the hostname in the service record no longer matches the hostname in either of the
|
||||
// address records, then drop the address records.
|
||||
if (this.serviceRecord != null) {
|
||||
boolean dropAddressRecords = false;
|
||||
|
||||
if (this.inet4AddressRecord != null) {
|
||||
if (!Arrays.equals(
|
||||
this.serviceRecord.getServiceHost(), this.inet4AddressRecord.getName())) {
|
||||
dropAddressRecords = true;
|
||||
}
|
||||
}
|
||||
if (this.inet6AddressRecord != null) {
|
||||
if (!Arrays.equals(
|
||||
this.serviceRecord.getServiceHost(), this.inet6AddressRecord.getName())) {
|
||||
dropAddressRecords = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (dropAddressRecords) {
|
||||
setInet4AddressRecord(null);
|
||||
setInet6AddressRecord(null);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the response is complete. A response is considered complete if it contains PTR, SRV,
|
||||
* TXT, and A (for IPv4) or AAAA (for IPv6) records.
|
||||
*/
|
||||
public synchronized boolean isComplete() {
|
||||
return !pointerRecords.isEmpty()
|
||||
&& (serviceRecord != null)
|
||||
&& (textRecord != null)
|
||||
&& (inet4AddressRecord != null || inet6AddressRecord != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the key for this response. The key uniquely identifies the response by its service
|
||||
* name.
|
||||
*/
|
||||
public synchronized String getServiceInstanceName() {
|
||||
if (pointerRecords.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String[] pointers = pointerRecords.get(0).getPointer();
|
||||
return ((pointers != null) && (pointers.length > 0)) ? pointers[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if this response is a goodbye message. This will be true if a service record is present
|
||||
* and any of the records have a TTL of 0.
|
||||
*/
|
||||
public synchronized boolean isGoodbye() {
|
||||
if (getServiceInstanceName() != null) {
|
||||
for (MdnsRecord record : records) {
|
||||
// Expiring PTR records with subtypes just signal a change in known supported
|
||||
// criteria, not the device itself going offline, so ignore those.
|
||||
if ((record instanceof MdnsPointerRecord)
|
||||
&& ((MdnsPointerRecord) record).hasSubtype()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.getTtl() == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the response to a packet.
|
||||
*
|
||||
* @param writer The writer to use.
|
||||
* @param now The current time. This is used to write updated TTLs that reflect the remaining
|
||||
* TTL
|
||||
* since the response was received.
|
||||
* @return The number of records that were written.
|
||||
* @throws IOException If an error occurred while writing (typically indicating overflow).
|
||||
*/
|
||||
public synchronized int write(MdnsPacketWriter writer, long now) throws IOException {
|
||||
int count = 0;
|
||||
for (MdnsPointerRecord pointerRecord : pointerRecords) {
|
||||
pointerRecord.write(writer, now);
|
||||
++count;
|
||||
}
|
||||
|
||||
if (serviceRecord != null) {
|
||||
serviceRecord.write(writer, now);
|
||||
++count;
|
||||
}
|
||||
|
||||
if (textRecord != null) {
|
||||
textRecord.write(writer, now);
|
||||
++count;
|
||||
}
|
||||
|
||||
if (inet4AddressRecord != null) {
|
||||
inet4AddressRecord.write(writer, now);
|
||||
++count;
|
||||
}
|
||||
|
||||
if (inet6AddressRecord != null) {
|
||||
inet6AddressRecord.write(writer, now);
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.server.connectivity.mdns.util.MdnsLogger;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/** A class that decodes mDNS responses from UDP packets. */
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
public class MdnsResponseDecoder {
|
||||
|
||||
public static final int SUCCESS = 0;
|
||||
private static final String TAG = "MdnsResponseDecoder";
|
||||
private static final MdnsLogger LOGGER = new MdnsLogger(TAG);
|
||||
private final String[] serviceType;
|
||||
private final Clock clock;
|
||||
|
||||
/** Constructs a new decoder that will extract responses for the given service type. */
|
||||
public MdnsResponseDecoder(@NonNull Clock clock, @Nullable String[] serviceType) {
|
||||
this.clock = clock;
|
||||
this.serviceType = serviceType;
|
||||
}
|
||||
|
||||
private static void skipMdnsRecord(MdnsPacketReader reader) throws IOException {
|
||||
reader.skip(2 + 4); // skip the class and TTL
|
||||
int dataLength = reader.readUInt16();
|
||||
reader.skip(dataLength);
|
||||
}
|
||||
|
||||
private static MdnsResponse findResponseWithPointer(
|
||||
List<MdnsResponse> responses, String[] pointer) {
|
||||
if (responses != null) {
|
||||
for (MdnsResponse response : responses) {
|
||||
List<MdnsPointerRecord> pointerRecords = response.getPointerRecords();
|
||||
if (pointerRecords == null) {
|
||||
continue;
|
||||
}
|
||||
for (MdnsPointerRecord pointerRecord : pointerRecords) {
|
||||
if (Arrays.equals(pointerRecord.getPointer(), pointer)) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static MdnsResponse findResponseWithHostName(
|
||||
List<MdnsResponse> responses, String[] hostName) {
|
||||
if (responses != null) {
|
||||
for (MdnsResponse response : responses) {
|
||||
MdnsServiceRecord serviceRecord = response.getServiceRecord();
|
||||
if (serviceRecord == null) {
|
||||
continue;
|
||||
}
|
||||
if (Arrays.equals(serviceRecord.getServiceHost(), hostName)) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes all mDNS responses for the desired service type from a packet. The class does not
|
||||
* check
|
||||
* the responses for completeness; the caller should do that.
|
||||
*
|
||||
* @param packet The packet to read from.
|
||||
* @return A list of mDNS responses, or null if the packet contained no appropriate responses.
|
||||
*/
|
||||
public int decode(@NonNull DatagramPacket packet, @NonNull List<MdnsResponse> responses) {
|
||||
MdnsPacketReader reader = new MdnsPacketReader(packet);
|
||||
|
||||
List<MdnsRecord> records;
|
||||
try {
|
||||
reader.readUInt16(); // transaction ID (not used)
|
||||
int flags = reader.readUInt16();
|
||||
if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
|
||||
return MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE;
|
||||
}
|
||||
|
||||
int numQuestions = reader.readUInt16();
|
||||
int numAnswers = reader.readUInt16();
|
||||
int numAuthority = reader.readUInt16();
|
||||
int numRecords = reader.readUInt16();
|
||||
|
||||
LOGGER.log(String.format(
|
||||
"num questions: %d, num answers: %d, num authority: %d, num records: %d",
|
||||
numQuestions, numAnswers, numAuthority, numRecords));
|
||||
|
||||
if (numAnswers < 1) {
|
||||
return MdnsResponseErrorCode.ERROR_NO_ANSWERS;
|
||||
}
|
||||
|
||||
records = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < (numAnswers + numAuthority + numRecords); ++i) {
|
||||
String[] name;
|
||||
try {
|
||||
name = reader.readLabels();
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to read labels from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_READING_RECORD_NAME;
|
||||
}
|
||||
int type = reader.readUInt16();
|
||||
|
||||
switch (type) {
|
||||
case MdnsRecord.TYPE_A: {
|
||||
try {
|
||||
records.add(new MdnsInetAddressRecord(name, MdnsRecord.TYPE_A, reader));
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to read A record from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_READING_A_RDATA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MdnsRecord.TYPE_AAAA: {
|
||||
try {
|
||||
// AAAA should only contain the IPv6 address.
|
||||
MdnsInetAddressRecord record =
|
||||
new MdnsInetAddressRecord(name, MdnsRecord.TYPE_AAAA, reader);
|
||||
if (record.getInet6Address() != null) {
|
||||
records.add(record);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to read AAAA record from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_READING_AAAA_RDATA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MdnsRecord.TYPE_PTR: {
|
||||
try {
|
||||
records.add(new MdnsPointerRecord(name, reader));
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to read PTR record from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_READING_PTR_RDATA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MdnsRecord.TYPE_SRV: {
|
||||
if (name.length == 4) {
|
||||
try {
|
||||
records.add(new MdnsServiceRecord(name, reader));
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to read SRV record from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_READING_SRV_RDATA;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
skipMdnsRecord(reader);
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to skip SVR record from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_SKIPPING_SRV_RDATA;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MdnsRecord.TYPE_TXT: {
|
||||
try {
|
||||
records.add(new MdnsTextRecord(name, reader));
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to read TXT record from mDNS response.", e);
|
||||
return MdnsResponseErrorCode.ERROR_READING_TXT_RDATA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
try {
|
||||
skipMdnsRecord(reader);
|
||||
} catch (IOException e) {
|
||||
LOGGER.e("Failed to skip mDNS record.", e);
|
||||
return MdnsResponseErrorCode.ERROR_SKIPPING_UNKNOWN_RECORD;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
LOGGER.e("Reached the end of the mDNS response unexpectedly.", e);
|
||||
return MdnsResponseErrorCode.ERROR_END_OF_FILE;
|
||||
}
|
||||
|
||||
// The response records are structured in a hierarchy, where some records reference
|
||||
// others, as follows:
|
||||
//
|
||||
// PTR
|
||||
// / \
|
||||
// / \
|
||||
// TXT SRV
|
||||
// / \
|
||||
// / \
|
||||
// A AAAA
|
||||
//
|
||||
// But the order in which these records appear in the response packet is completely
|
||||
// arbitrary. This means that we need to rescan the record list to construct each level of
|
||||
// this hierarchy.
|
||||
//
|
||||
// PTR: service type -> service instance name
|
||||
//
|
||||
// SRV: service instance name -> host name (priority, weight)
|
||||
//
|
||||
// TXT: service instance name -> machine readable txt entries.
|
||||
//
|
||||
// A: host name -> IP address
|
||||
|
||||
// Loop 1: find PTR records, which identify distinct service instances.
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
for (MdnsRecord record : records) {
|
||||
if (record instanceof MdnsPointerRecord) {
|
||||
String[] name = record.getName();
|
||||
if ((serviceType == null)
|
||||
|| Arrays.equals(name, serviceType)
|
||||
|| ((name.length == (serviceType.length + 2))
|
||||
&& name[1].equals(MdnsConstants.SUBTYPE_LABEL)
|
||||
&& MdnsRecord.labelsAreSuffix(serviceType, name))) {
|
||||
MdnsPointerRecord pointerRecord = (MdnsPointerRecord) record;
|
||||
// Group PTR records that refer to the same service instance name into a single
|
||||
// response.
|
||||
MdnsResponse response = findResponseWithPointer(responses,
|
||||
pointerRecord.getPointer());
|
||||
if (response == null) {
|
||||
response = new MdnsResponse(now);
|
||||
responses.add(response);
|
||||
}
|
||||
response.addPointerRecord((MdnsPointerRecord) record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop 2: find SRV and TXT records, which reference the pointer in the PTR record.
|
||||
for (MdnsRecord record : records) {
|
||||
if (record instanceof MdnsServiceRecord) {
|
||||
MdnsServiceRecord serviceRecord = (MdnsServiceRecord) record;
|
||||
MdnsResponse response = findResponseWithPointer(responses, serviceRecord.getName());
|
||||
if (response != null) {
|
||||
response.setServiceRecord(serviceRecord);
|
||||
}
|
||||
} else if (record instanceof MdnsTextRecord) {
|
||||
MdnsTextRecord textRecord = (MdnsTextRecord) record;
|
||||
MdnsResponse response = findResponseWithPointer(responses, textRecord.getName());
|
||||
if (response != null) {
|
||||
response.setTextRecord(textRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop 3: find A and AAAA records, which reference the host name in the SRV record.
|
||||
for (MdnsRecord record : records) {
|
||||
if (record instanceof MdnsInetAddressRecord) {
|
||||
MdnsInetAddressRecord inetRecord = (MdnsInetAddressRecord) record;
|
||||
MdnsResponse response = findResponseWithHostName(responses, inetRecord.getName());
|
||||
if (inetRecord.getInet4Address() != null && response != null) {
|
||||
response.setInet4AddressRecord(inetRecord);
|
||||
} else if (inetRecord.getInet6Address() != null && response != null) {
|
||||
response.setInet6AddressRecord(inetRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
public static class Clock {
|
||||
public long elapsedRealtime() {
|
||||
return SystemClock.elapsedRealtime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
/**
|
||||
* The list of error code for parsing mDNS response.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class MdnsResponseErrorCode {
|
||||
public static final int SUCCESS = 0;
|
||||
public static final int ERROR_NOT_RESPONSE_MESSAGE = 1;
|
||||
public static final int ERROR_NO_ANSWERS = 2;
|
||||
public static final int ERROR_READING_RECORD_NAME = 3;
|
||||
public static final int ERROR_READING_A_RDATA = 4;
|
||||
public static final int ERROR_READING_AAAA_RDATA = 5;
|
||||
public static final int ERROR_READING_PTR_RDATA = 6;
|
||||
public static final int ERROR_SKIPPING_PTR_RDATA = 7;
|
||||
public static final int ERROR_READING_SRV_RDATA = 8;
|
||||
public static final int ERROR_SKIPPING_SRV_RDATA = 9;
|
||||
public static final int ERROR_READING_TXT_RDATA = 10;
|
||||
public static final int ERROR_SKIPPING_UNKNOWN_RECORD = 11;
|
||||
public static final int ERROR_END_OF_FILE = 12;
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* API configuration parameters for searching the mDNS service.
|
||||
*
|
||||
* <p>Use {@link MdnsSearchOptions.Builder} to create {@link MdnsSearchOptions}.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class MdnsSearchOptions implements Parcelable {
|
||||
|
||||
/** @hide */
|
||||
public static final Parcelable.Creator<MdnsSearchOptions> CREATOR =
|
||||
new Parcelable.Creator<MdnsSearchOptions>() {
|
||||
@Override
|
||||
public MdnsSearchOptions createFromParcel(Parcel source) {
|
||||
return new MdnsSearchOptions(source.createStringArrayList(),
|
||||
source.readBoolean());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MdnsSearchOptions[] newArray(int size) {
|
||||
return new MdnsSearchOptions[size];
|
||||
}
|
||||
};
|
||||
private static MdnsSearchOptions defaultOptions;
|
||||
private final List<String> subtypes;
|
||||
|
||||
private final boolean isPassiveMode;
|
||||
|
||||
/** Parcelable constructs for a {@link MdnsServiceInfo}. */
|
||||
MdnsSearchOptions(List<String> subtypes, boolean isPassiveMode) {
|
||||
this.subtypes = new ArrayList<>();
|
||||
if (subtypes != null) {
|
||||
this.subtypes.addAll(subtypes);
|
||||
}
|
||||
this.isPassiveMode = isPassiveMode;
|
||||
}
|
||||
|
||||
/** Returns a {@link Builder} for {@link MdnsSearchOptions}. */
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/** Returns a default search options. */
|
||||
public static synchronized MdnsSearchOptions getDefaultOptions() {
|
||||
if (defaultOptions == null) {
|
||||
defaultOptions = newBuilder().build();
|
||||
}
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
/** @return the list of subtypes to search. */
|
||||
public List<String> getSubtypes() {
|
||||
return subtypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the passive mode is used. The passive mode scans less frequently in
|
||||
* order to conserve battery and produce less network traffic.
|
||||
*/
|
||||
public boolean isPassiveMode() {
|
||||
return isPassiveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeStringList(subtypes);
|
||||
out.writeBoolean(isPassiveMode);
|
||||
}
|
||||
|
||||
/** A builder to create {@link MdnsSearchOptions}. */
|
||||
public static final class Builder {
|
||||
private final Set<String> subtypes;
|
||||
private boolean isPassiveMode = true;
|
||||
|
||||
private Builder() {
|
||||
subtypes = new ArraySet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a subtype to search.
|
||||
*
|
||||
* @param subtype the subtype to add.
|
||||
*/
|
||||
public Builder addSubtype(@NonNull String subtype) {
|
||||
if (TextUtils.isEmpty(subtype)) {
|
||||
throw new IllegalArgumentException("Empty subtype");
|
||||
}
|
||||
subtypes.add(subtype);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of subtypes to search.
|
||||
*
|
||||
* @param subtypes The list of subtypes to add.
|
||||
*/
|
||||
public Builder addSubtypes(@NonNull Collection<String> subtypes) {
|
||||
this.subtypes.addAll(Objects.requireNonNull(subtypes));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the passive mode scan should be used. The passive mode scans less frequently in
|
||||
* order
|
||||
* to conserve battery and produce less network traffic.
|
||||
*
|
||||
* @param isPassiveMode If set to {@code true}, passive mode will be used. If set to {@code
|
||||
* false}, active mode will be used.
|
||||
*/
|
||||
public Builder setIsPassiveMode(boolean isPassiveMode) {
|
||||
this.isPassiveMode = isPassiveMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds a {@link MdnsSearchOptions} with the arguments supplied to this builder. */
|
||||
public MdnsSearchOptions build() {
|
||||
return new MdnsSearchOptions(new ArrayList<>(subtypes), isPassiveMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Listener interface for mDNS service instance discovery events.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public interface MdnsServiceBrowserListener {
|
||||
|
||||
/**
|
||||
* Called when an mDNS service instance is found.
|
||||
*
|
||||
* @param serviceInfo The found mDNS service instance.
|
||||
*/
|
||||
void onServiceFound(@NonNull MdnsServiceInfo serviceInfo);
|
||||
|
||||
/**
|
||||
* Called when an mDNS service instance is updated.
|
||||
*
|
||||
* @param serviceInfo The updated mDNS service instance.
|
||||
*/
|
||||
void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo);
|
||||
|
||||
/**
|
||||
* Called when an mDNS service instance is no longer valid and removed.
|
||||
*
|
||||
* @param serviceInstanceName The service instance name of the removed mDNS service.
|
||||
*/
|
||||
void onServiceRemoved(@NonNull String serviceInstanceName);
|
||||
|
||||
/**
|
||||
* Called when searching for mDNS service has stopped because of an error.
|
||||
*
|
||||
* TODO (changed when importing code): define error constants
|
||||
*
|
||||
* @param error The error code of the stop reason.
|
||||
*/
|
||||
void onSearchStoppedWithError(int error);
|
||||
|
||||
/** Called when it failed to start an mDNS service discovery process. */
|
||||
void onSearchFailedToStart();
|
||||
|
||||
/**
|
||||
* Called when a mDNS service discovery query has been sent.
|
||||
*
|
||||
* @param subtypes The list of subtypes in the discovery query.
|
||||
* @param transactionId The transaction ID of the query.
|
||||
*/
|
||||
void onDiscoveryQuerySent(@NonNull List<String> subtypes, int transactionId);
|
||||
|
||||
/**
|
||||
* Called when an error has happened when parsing a received mDNS response packet.
|
||||
*
|
||||
* @param receivedPacketNumber The packet sequence number of the received packet.
|
||||
* @param errorCode The error code, defined in {@link MdnsResponseErrorCode}.
|
||||
*/
|
||||
void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A class representing a discovered mDNS service instance.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class MdnsServiceInfo implements Parcelable {
|
||||
|
||||
/** @hide */
|
||||
public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
|
||||
new Parcelable.Creator<MdnsServiceInfo>() {
|
||||
|
||||
@Override
|
||||
public MdnsServiceInfo createFromParcel(Parcel source) {
|
||||
return new MdnsServiceInfo(
|
||||
source.readString(),
|
||||
source.createStringArray(),
|
||||
source.createStringArrayList(),
|
||||
source.createStringArray(),
|
||||
source.readInt(),
|
||||
source.readString(),
|
||||
source.readString(),
|
||||
source.createStringArrayList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MdnsServiceInfo[] newArray(int size) {
|
||||
return new MdnsServiceInfo[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final String serviceInstanceName;
|
||||
private final String[] serviceType;
|
||||
private final List<String> subtypes;
|
||||
private final String[] hostName;
|
||||
private final int port;
|
||||
private final String ipv4Address;
|
||||
private final String ipv6Address;
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
List<String> textStrings;
|
||||
|
||||
/**
|
||||
* Constructs a {@link MdnsServiceInfo} object with default values.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public MdnsServiceInfo(
|
||||
String serviceInstanceName,
|
||||
String[] serviceType,
|
||||
List<String> subtypes,
|
||||
String[] hostName,
|
||||
int port,
|
||||
String ipv4Address,
|
||||
String ipv6Address,
|
||||
List<String> textStrings) {
|
||||
this.serviceInstanceName = serviceInstanceName;
|
||||
this.serviceType = serviceType;
|
||||
this.subtypes = new ArrayList<>();
|
||||
if (subtypes != null) {
|
||||
this.subtypes.addAll(subtypes);
|
||||
}
|
||||
this.hostName = hostName;
|
||||
this.port = port;
|
||||
this.ipv4Address = ipv4Address;
|
||||
this.ipv6Address = ipv6Address;
|
||||
if (textStrings != null) {
|
||||
for (String text : textStrings) {
|
||||
int pos = text.indexOf('=');
|
||||
if (pos < 1) {
|
||||
continue;
|
||||
}
|
||||
attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
|
||||
text.substring(++pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return the name of this service instance. */
|
||||
public String getServiceInstanceName() {
|
||||
return serviceInstanceName;
|
||||
}
|
||||
|
||||
/** @return the type of this service instance. */
|
||||
public String[] getServiceType() {
|
||||
return serviceType;
|
||||
}
|
||||
|
||||
/** @return the list of subtypes supported by this service instance. */
|
||||
public List<String> getSubtypes() {
|
||||
return new ArrayList<>(subtypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if this service instance supports any subtypes.
|
||||
* @return {@code false} if this service instance does not support any subtypes.
|
||||
*/
|
||||
public boolean hasSubtypes() {
|
||||
return !subtypes.isEmpty();
|
||||
}
|
||||
|
||||
/** @return the host name of this service instance. */
|
||||
public String[] getHostName() {
|
||||
return hostName;
|
||||
}
|
||||
|
||||
/** @return the port number of this service instance. */
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/** @return the IPV4 address of this service instance. */
|
||||
public String getIpv4Address() {
|
||||
return ipv4Address;
|
||||
}
|
||||
|
||||
/** @return the IPV6 address of this service instance. */
|
||||
public String getIpv6Address() {
|
||||
return ipv6Address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the attribute value for {@code key}.
|
||||
* @return {@code null} if no attribute value exists for {@code key}.
|
||||
*/
|
||||
public String getAttributeByKey(@NonNull String key) {
|
||||
return attributes.get(key.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
/** @return an immutable map of all attributes. */
|
||||
public Map<String, String> getAttributes() {
|
||||
return Collections.unmodifiableMap(attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
if (textStrings == null) {
|
||||
// Lazily initialize the parcelable field mTextStrings.
|
||||
textStrings = new ArrayList<>(attributes.size());
|
||||
for (Map.Entry<String, String> kv : attributes.entrySet()) {
|
||||
textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
out.writeString(serviceInstanceName);
|
||||
out.writeStringArray(serviceType);
|
||||
out.writeStringList(subtypes);
|
||||
out.writeStringArray(hostName);
|
||||
out.writeInt(port);
|
||||
out.writeString(ipv4Address);
|
||||
out.writeString(ipv6Address);
|
||||
out.writeStringList(textStrings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"Name: %s, subtypes: %s, ip: %s, port: %d",
|
||||
serviceInstanceName,
|
||||
TextUtils.join(",", subtypes),
|
||||
ipv4Address,
|
||||
port);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.connectivity.mdns;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
/** An mDNS "SRV" record, which contains service information. */
|
||||
// TODO(b/177655645): Resolve nullness suppression.
|
||||
@SuppressWarnings("nullness")
|
||||
@VisibleForTesting
|
||||
public class MdnsServiceRecord extends MdnsRecord {
|
||||
public static final int PROTO_NONE = 0;
|
||||
public static final int PROTO_TCP = 1;
|
||||
public static final int PROTO_UDP = 2;
|
||||
private static final String PROTO_TOKEN_TCP = "_tcp";
|
||||
private static final String PROTO_TOKEN_UDP = "_udp";
|
||||
private int servicePriority;
|
||||
private int serviceWeight;
|
||||
private int servicePort;
|
||||
private String[] serviceHost;
|
||||
|
||||
public MdnsServiceRecord(String[] name, MdnsPacketReader reader) throws IOException {
|
||||
super(name, TYPE_SRV, reader);
|
||||
}
|
||||
|
||||
/** Returns the service's port number. */
|
||||
public int getServicePort() {
|
||||
return servicePort;
|
||||
}
|
||||
|
||||
/** Returns the service's host name. */
|
||||
public String[] getServiceHost() {
|
||||
return serviceHost;
|
||||
}
|
||||
|
||||
/** Returns the service's priority. */
|
||||
public int getServicePriority() {
|
||||
return servicePriority;
|
||||
}
|
||||
|
||||
/** Returns the service's weight. */
|
||||
public int getServiceWeight() {
|
||||
return serviceWeight;
|
||||
}
|
||||
|
||||
// Format of name is <instance-name>.<service-name>.<protocol>.<domain>
|
||||
|
||||
/** Returns the service's instance name, which uniquely identifies the service instance. */
|
||||
public String getServiceInstanceName() {
|
||||
if (name.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return name[0];
|
||||
}
|
||||
|
||||
/** Returns the service's name. */
|
||||
public String getServiceName() {
|
||||
if (name.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return name[1];
|
||||
}
|
||||
|
||||
/** Returns the service's protocol. */
|
||||
public int getServiceProtocol() {
|
||||
if (name.length < 3) {
|
||||
return PROTO_NONE;
|
||||
}
|
||||
|
||||
String protocol = name[2];
|
||||
if (protocol.equals(PROTO_TOKEN_TCP)) {
|
||||
return PROTO_TCP;
|
||||
}
|
||||
if (protocol.equals(PROTO_TOKEN_UDP)) {
|
||||
return PROTO_UDP;
|
||||
}
|
||||
return PROTO_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readData(MdnsPacketReader reader) throws IOException {
|
||||
servicePriority = reader.readUInt16();
|
||||
serviceWeight = reader.readUInt16();
|
||||
servicePort = reader.readUInt16();
|
||||
serviceHost = reader.readLabels();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeData(MdnsPacketWriter writer) throws IOException {
|
||||
writer.writeUInt16(servicePriority);
|
||||
writer.writeUInt16(serviceWeight);
|
||||
writer.writeUInt16(servicePort);
|
||||
writer.writeLabels(serviceHost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"SRV: %s:%d (prio=%d, weight=%d)",
|
||||
labelsToString(serviceHost),
|
||||
servicePort,
|
||||
servicePriority,
|
||||
serviceWeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (super.hashCode() * 31)
|
||||
+ Objects.hash(servicePriority, serviceWeight, Arrays.hashCode(serviceHost),
|
||||
servicePort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof MdnsServiceRecord)) {
|
||||
return false;
|
||||
}
|
||||
MdnsServiceRecord otherRecord = (MdnsServiceRecord) other;
|
||||
|
||||
return super.equals(other)
|
||||
&& (servicePriority == otherRecord.servicePriority)
|
||||
&& (serviceWeight == otherRecord.serviceWeight)
|
||||
&& Objects.equals(serviceHost, otherRecord.serviceHost)
|
||||
&& (servicePort == otherRecord.servicePort);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue