diff options
| -rwxr-xr-x | api/system-current.txt | 31 | ||||
| -rw-r--r-- | api/test-current.txt | 31 | ||||
| -rw-r--r-- | core/java/android/net/CaptivePortalData.aidl | 19 | ||||
| -rw-r--r-- | core/java/android/net/CaptivePortalData.java | 281 | ||||
| -rw-r--r-- | core/java/android/net/LinkProperties.java | 169 | ||||
| -rw-r--r-- | core/java/android/net/Uri.aidl | 2 | ||||
| -rw-r--r-- | services/core/java/com/android/server/ConnectivityService.java | 68 | ||||
| -rw-r--r-- | tests/net/common/java/android/net/LinkPropertiesTest.java | 41 | ||||
| -rw-r--r-- | tests/net/java/android/net/CaptivePortalDataTest.kt | 72 | ||||
| -rw-r--r-- | tests/net/java/com/android/server/ConnectivityServiceTest.java | 109 |
10 files changed, 773 insertions, 50 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index e2fb99ea2931..c3bde2ae26c5 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4294,6 +4294,32 @@ package android.net { field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2 } + public final class CaptivePortalData implements android.os.Parcelable { + method public int describeContents(); + method public long getByteLimit(); + method public long getExpiryTimeMillis(); + method public long getRefreshTimeMillis(); + method @Nullable public android.net.Uri getUserPortalUrl(); + method @Nullable public android.net.Uri getVenueInfoUrl(); + method public boolean isCaptive(); + method public boolean isSessionExtendable(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR; + } + + public static class CaptivePortalData.Builder { + ctor public CaptivePortalData.Builder(); + ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData); + method @NonNull public android.net.CaptivePortalData build(); + method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long); + method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean); + method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long); + method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long); + method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean); + method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri); + method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri); + } + public class ConnectivityManager { method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); @@ -4425,6 +4451,8 @@ package android.net { method @NonNull public java.util.List<java.lang.String> getAllInterfaceNames(); method @NonNull public java.util.List<android.net.LinkAddress> getAllLinkAddresses(); method @NonNull public java.util.List<android.net.RouteInfo> getAllRoutes(); + method @Nullable public android.net.Uri getCaptivePortalApiUrl(); + method @Nullable public android.net.CaptivePortalData getCaptivePortalData(); method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers(); method @Nullable public String getTcpBufferSizes(); method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers(); @@ -4438,9 +4466,12 @@ package android.net { method public boolean isIpv6Provisioned(); method public boolean isProvisioned(); method public boolean isReachable(@NonNull java.net.InetAddress); + method @NonNull public android.net.LinkProperties makeSensitiveFieldsParcelingCopy(); method public boolean removeDnsServer(@NonNull java.net.InetAddress); method public boolean removeLinkAddress(@NonNull android.net.LinkAddress); method public boolean removeRoute(@NonNull android.net.RouteInfo); + method public void setCaptivePortalApiUrl(@Nullable android.net.Uri); + method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData); method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>); method public void setPrivateDnsServerName(@Nullable String); method public void setTcpBufferSizes(@Nullable String); diff --git a/api/test-current.txt b/api/test-current.txt index e4997b76c1b9..abfd764b2b95 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -1361,6 +1361,32 @@ package android.net { field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2 } + public final class CaptivePortalData implements android.os.Parcelable { + method public int describeContents(); + method public long getByteLimit(); + method public long getExpiryTimeMillis(); + method public long getRefreshTimeMillis(); + method @Nullable public android.net.Uri getUserPortalUrl(); + method @Nullable public android.net.Uri getVenueInfoUrl(); + method public boolean isCaptive(); + method public boolean isSessionExtendable(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR; + } + + public static class CaptivePortalData.Builder { + ctor public CaptivePortalData.Builder(); + ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData); + method @NonNull public android.net.CaptivePortalData build(); + method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long); + method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean); + method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long); + method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long); + method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean); + method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri); + method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri); + } + public class ConnectivityManager { method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle); field public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC"; @@ -1391,6 +1417,8 @@ package android.net { ctor public LinkProperties(@Nullable android.net.LinkProperties); method public boolean addDnsServer(@NonNull java.net.InetAddress); method public boolean addLinkAddress(@NonNull android.net.LinkAddress); + method @Nullable public android.net.Uri getCaptivePortalApiUrl(); + method @Nullable public android.net.CaptivePortalData getCaptivePortalData(); method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers(); method @Nullable public String getTcpBufferSizes(); method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers(); @@ -1401,9 +1429,12 @@ package android.net { method public boolean isIpv6Provisioned(); method public boolean isProvisioned(); method public boolean isReachable(@NonNull java.net.InetAddress); + method @NonNull public android.net.LinkProperties makeSensitiveFieldsParcelingCopy(); method public boolean removeDnsServer(@NonNull java.net.InetAddress); method public boolean removeLinkAddress(@NonNull android.net.LinkAddress); method public boolean removeRoute(@NonNull android.net.RouteInfo); + method public void setCaptivePortalApiUrl(@Nullable android.net.Uri); + method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData); method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>); method public void setPrivateDnsServerName(@Nullable String); method public void setTcpBufferSizes(@Nullable String); diff --git a/core/java/android/net/CaptivePortalData.aidl b/core/java/android/net/CaptivePortalData.aidl new file mode 100644 index 000000000000..1d57ee759136 --- /dev/null +++ b/core/java/android/net/CaptivePortalData.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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 android.net; + +@JavaOnlyStableParcelable parcelable CaptivePortalData; diff --git a/core/java/android/net/CaptivePortalData.java b/core/java/android/net/CaptivePortalData.java new file mode 100644 index 000000000000..1357803a6cb8 --- /dev/null +++ b/core/java/android/net/CaptivePortalData.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2019 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 android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Metadata sent by captive portals, see https://www.ietf.org/id/draft-ietf-capport-api-03.txt. + * @hide + */ +@SystemApi +@TestApi +public final class CaptivePortalData implements Parcelable { + private final long mRefreshTimeMillis; + @Nullable + private final Uri mUserPortalUrl; + @Nullable + private final Uri mVenueInfoUrl; + private final boolean mIsSessionExtendable; + private final long mByteLimit; + private final long mExpiryTimeMillis; + private final boolean mCaptive; + + private CaptivePortalData(long refreshTimeMillis, Uri userPortalUrl, Uri venueInfoUrl, + boolean isSessionExtendable, long byteLimit, long expiryTimeMillis, boolean captive) { + mRefreshTimeMillis = refreshTimeMillis; + mUserPortalUrl = userPortalUrl; + mVenueInfoUrl = venueInfoUrl; + mIsSessionExtendable = isSessionExtendable; + mByteLimit = byteLimit; + mExpiryTimeMillis = expiryTimeMillis; + mCaptive = captive; + } + + private CaptivePortalData(Parcel p) { + this(p.readLong(), p.readParcelable(null), p.readParcelable(null), p.readBoolean(), + p.readLong(), p.readLong(), p.readBoolean()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mRefreshTimeMillis); + dest.writeParcelable(mUserPortalUrl, 0); + dest.writeParcelable(mVenueInfoUrl, 0); + dest.writeBoolean(mIsSessionExtendable); + dest.writeLong(mByteLimit); + dest.writeLong(mExpiryTimeMillis); + dest.writeBoolean(mCaptive); + } + + /** + * A builder to create new {@link CaptivePortalData}. + */ + public static class Builder { + private long mRefreshTime; + private Uri mUserPortalUrl; + private Uri mVenueInfoUrl; + private boolean mIsSessionExtendable; + private long mBytesRemaining = -1; + private long mExpiryTime = -1; + private boolean mCaptive; + + /** + * Create an empty builder. + */ + public Builder() {} + + /** + * Create a builder copying all data from existing {@link CaptivePortalData}. + */ + public Builder(@Nullable CaptivePortalData data) { + if (data == null) return; + setRefreshTime(data.mRefreshTimeMillis) + .setUserPortalUrl(data.mUserPortalUrl) + .setVenueInfoUrl(data.mVenueInfoUrl) + .setSessionExtendable(data.mIsSessionExtendable) + .setBytesRemaining(data.mByteLimit) + .setExpiryTime(data.mExpiryTimeMillis) + .setCaptive(data.mCaptive); + } + + /** + * Set the time at which data was last refreshed, as per {@link System#currentTimeMillis()}. + */ + @NonNull + public Builder setRefreshTime(long refreshTime) { + mRefreshTime = refreshTime; + return this; + } + + /** + * Set the URL to be used for users to login to the portal, if captive. + */ + @NonNull + public Builder setUserPortalUrl(@Nullable Uri userPortalUrl) { + mUserPortalUrl = userPortalUrl; + return this; + } + + /** + * Set the URL that can be used by users to view information about the network venue. + */ + @NonNull + public Builder setVenueInfoUrl(@Nullable Uri venueInfoUrl) { + mVenueInfoUrl = venueInfoUrl; + return this; + } + + /** + * Set whether the portal supports extending a user session on the portal URL page. + */ + @NonNull + public Builder setSessionExtendable(boolean sessionExtendable) { + mIsSessionExtendable = sessionExtendable; + return this; + } + + /** + * Set the number of bytes remaining on the network before the portal closes. + */ + @NonNull + public Builder setBytesRemaining(long bytesRemaining) { + mBytesRemaining = bytesRemaining; + return this; + } + + /** + * Set the time at the session will expire, as per {@link System#currentTimeMillis()}. + */ + @NonNull + public Builder setExpiryTime(long expiryTime) { + mExpiryTime = expiryTime; + return this; + } + + /** + * Set whether the network is captive (portal closed). + */ + @NonNull + public Builder setCaptive(boolean captive) { + mCaptive = captive; + return this; + } + + /** + * Create a new {@link CaptivePortalData}. + */ + @NonNull + public CaptivePortalData build() { + return new CaptivePortalData(mRefreshTime, mUserPortalUrl, mVenueInfoUrl, + mIsSessionExtendable, mBytesRemaining, mExpiryTime, mCaptive); + } + } + + /** + * Get the time at which data was last refreshed, as per {@link System#currentTimeMillis()}. + */ + public long getRefreshTimeMillis() { + return mRefreshTimeMillis; + } + + /** + * Get the URL to be used for users to login to the portal, or extend their session if + * {@link #isSessionExtendable()} is true. + */ + @Nullable + public Uri getUserPortalUrl() { + return mUserPortalUrl; + } + + /** + * Get the URL that can be used by users to view information about the network venue. + */ + @Nullable + public Uri getVenueInfoUrl() { + return mVenueInfoUrl; + } + + /** + * Indicates whether the user portal URL can be used to extend sessions, when the user is logged + * in and the session has a time or byte limit. + */ + public boolean isSessionExtendable() { + return mIsSessionExtendable; + } + + /** + * Get the remaining bytes on the captive portal session, at the time {@link CaptivePortalData} + * was refreshed. This may be different from the limit currently enforced by the portal. + * @return The byte limit, or -1 if not set. + */ + public long getByteLimit() { + return mByteLimit; + } + + /** + * Get the time at the session will expire, as per {@link System#currentTimeMillis()}. + * @return The expiry time, or -1 if unset. + */ + public long getExpiryTimeMillis() { + return mExpiryTimeMillis; + } + + /** + * Get whether the network is captive (portal closed). + */ + public boolean isCaptive() { + return mCaptive; + } + + @NonNull + public static final Creator<CaptivePortalData> CREATOR = new Creator<CaptivePortalData>() { + @Override + public CaptivePortalData createFromParcel(Parcel source) { + return new CaptivePortalData(source); + } + + @Override + public CaptivePortalData[] newArray(int size) { + return new CaptivePortalData[size]; + } + }; + + @Override + public int hashCode() { + return Objects.hash(mRefreshTimeMillis, mUserPortalUrl, mVenueInfoUrl, + mIsSessionExtendable, mByteLimit, mExpiryTimeMillis, mCaptive); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CaptivePortalData)) return false; + final CaptivePortalData other = (CaptivePortalData) obj; + return mRefreshTimeMillis == other.mRefreshTimeMillis + && Objects.equals(mUserPortalUrl, other.mUserPortalUrl) + && Objects.equals(mVenueInfoUrl, other.mVenueInfoUrl) + && mIsSessionExtendable == other.mIsSessionExtendable + && mByteLimit == other.mByteLimit + && mExpiryTimeMillis == other.mExpiryTimeMillis + && mCaptive == other.mCaptive; + } + + @Override + public String toString() { + return "CaptivePortalData {" + + "refreshTime: " + mRefreshTimeMillis + + ", userPortalUrl: " + mUserPortalUrl + + ", venueInfoUrl: " + mVenueInfoUrl + + ", isSessionExtendable: " + mIsSessionExtendable + + ", byteLimit: " + mByteLimit + + ", expiryTime: " + mExpiryTimeMillis + + ", captive: " + mCaptive + + "}"; + } +} diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index 2792c564988a..ec773ef4a1e2 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -70,6 +70,14 @@ public final class LinkProperties implements Parcelable { private String mTcpBufferSizes; private IpPrefix mNat64Prefix; private boolean mWakeOnLanSupported; + private Uri mCaptivePortalApiUrl; + private CaptivePortalData mCaptivePortalData; + + /** + * Indicates whether parceling should preserve fields that are set based on permissions of + * the process receiving the {@link LinkProperties}. + */ + private final transient boolean mParcelSensitiveFields; private static final int MIN_MTU = 68; private static final int MIN_MTU_V6 = 1280; @@ -174,6 +182,7 @@ public final class LinkProperties implements Parcelable { * Constructs a new {@code LinkProperties} with default values. */ public LinkProperties() { + mParcelSensitiveFields = false; } /** @@ -182,26 +191,32 @@ public final class LinkProperties implements Parcelable { @SystemApi @TestApi public LinkProperties(@Nullable LinkProperties source) { - if (source != null) { - mIfaceName = source.mIfaceName; - mLinkAddresses.addAll(source.mLinkAddresses); - mDnses.addAll(source.mDnses); - mValidatedPrivateDnses.addAll(source.mValidatedPrivateDnses); - mUsePrivateDns = source.mUsePrivateDns; - mPrivateDnsServerName = source.mPrivateDnsServerName; - mPcscfs.addAll(source.mPcscfs); - mDomains = source.mDomains; - mRoutes.addAll(source.mRoutes); - mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy); - for (LinkProperties l: source.mStackedLinks.values()) { - addStackedLink(l); - } - setMtu(source.mMtu); - setDhcpServerAddress(source.getDhcpServerAddress()); - mTcpBufferSizes = source.mTcpBufferSizes; - mNat64Prefix = source.mNat64Prefix; - mWakeOnLanSupported = source.mWakeOnLanSupported; + this(source, false /* parcelSensitiveFields */); + } + + private LinkProperties(@Nullable LinkProperties source, boolean parcelSensitiveFields) { + mParcelSensitiveFields = parcelSensitiveFields; + if (source == null) return; + mIfaceName = source.mIfaceName; + mLinkAddresses.addAll(source.mLinkAddresses); + mDnses.addAll(source.mDnses); + mValidatedPrivateDnses.addAll(source.mValidatedPrivateDnses); + mUsePrivateDns = source.mUsePrivateDns; + mPrivateDnsServerName = source.mPrivateDnsServerName; + mPcscfs.addAll(source.mPcscfs); + mDomains = source.mDomains; + mRoutes.addAll(source.mRoutes); + mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy); + for (LinkProperties l: source.mStackedLinks.values()) { + addStackedLink(l); } + setMtu(source.mMtu); + setDhcpServerAddress(source.getDhcpServerAddress()); + mTcpBufferSizes = source.mTcpBufferSizes; + mNat64Prefix = source.mNat64Prefix; + mWakeOnLanSupported = source.mWakeOnLanSupported; + mCaptivePortalApiUrl = source.mCaptivePortalApiUrl; + mCaptivePortalData = source.mCaptivePortalData; } /** @@ -860,6 +875,11 @@ public final class LinkProperties implements Parcelable { * Clears this object to its initial state. */ public void clear() { + if (mParcelSensitiveFields) { + throw new UnsupportedOperationException( + "Cannot clear LinkProperties when parcelSensitiveFields is set"); + } + mIfaceName = null; mLinkAddresses.clear(); mDnses.clear(); @@ -875,6 +895,8 @@ public final class LinkProperties implements Parcelable { mTcpBufferSizes = null; mNat64Prefix = null; mWakeOnLanSupported = false; + mCaptivePortalApiUrl = null; + mCaptivePortalData = null; } /** @@ -945,6 +967,14 @@ public final class LinkProperties implements Parcelable { resultJoiner.add(mDhcpServerAddress.toString()); } + if (mCaptivePortalApiUrl != null) { + resultJoiner.add("CaptivePortalApiUrl: " + mCaptivePortalApiUrl); + } + + if (mCaptivePortalData != null) { + resultJoiner.add("CaptivePortalData: " + mCaptivePortalData); + } + if (mTcpBufferSizes != null) { resultJoiner.add("TcpBufferSizes:"); resultJoiner.add(mTcpBufferSizes); @@ -1479,6 +1509,28 @@ public final class LinkProperties implements Parcelable { } /** + * Compares this {@code LinkProperties}'s CaptivePortalApiUrl against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalCaptivePortalApiUrl(LinkProperties target) { + return Objects.equals(mCaptivePortalApiUrl, target.mCaptivePortalApiUrl); + } + + /** + * Compares this {@code LinkProperties}'s CaptivePortalData against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalCaptivePortalData(LinkProperties target) { + return Objects.equals(mCaptivePortalData, target.mCaptivePortalData); + } + + /** * Set whether the network interface supports WakeOnLAN * * @param supported WakeOnLAN supported value @@ -1499,6 +1551,73 @@ public final class LinkProperties implements Parcelable { } /** + * Set the URL of the captive portal API endpoint to get more information about the network. + * @hide + */ + @SystemApi + @TestApi + public void setCaptivePortalApiUrl(@Nullable Uri url) { + mCaptivePortalApiUrl = url; + } + + /** + * Get the URL of the captive portal API endpoint to get more information about the network. + * + * <p>This is null unless the application has + * {@link android.Manifest.permission.NETWORK_SETTINGS} or + * {@link NetworkStack#PERMISSION_MAINLINE_NETWORK_STACK} permissions, and the network provided + * the URL. + * @hide + */ + @SystemApi + @TestApi + @Nullable + public Uri getCaptivePortalApiUrl() { + return mCaptivePortalApiUrl; + } + + /** + * Set the CaptivePortalData obtained from the captive portal API (RFC7710bis). + * @hide + */ + @SystemApi + @TestApi + public void setCaptivePortalData(@Nullable CaptivePortalData data) { + mCaptivePortalData = data; + } + + /** + * Get the CaptivePortalData obtained from the captive portal API (RFC7710bis). + * + * <p>This is null unless the application has + * {@link android.Manifest.permission.NETWORK_SETTINGS} or + * {@link NetworkStack#PERMISSION_MAINLINE_NETWORK_STACK} permissions. + * @hide + */ + @SystemApi + @TestApi + @Nullable + public CaptivePortalData getCaptivePortalData() { + return mCaptivePortalData; + } + + /** + * Create a copy of this {@link LinkProperties} that will preserve fields that were set + * based on the permissions of the process that received this {@link LinkProperties}. + * + * <p>By default {@link LinkProperties} does not preserve such fields during parceling, as + * they should not be shared outside of the process that receives them without appropriate + * checks. + * @hide + */ + @SystemApi + @TestApi + @NonNull + public LinkProperties makeSensitiveFieldsParcelingCopy() { + return new LinkProperties(this, true /* parcelSensitiveFields */); + } + + /** * Compares this {@code LinkProperties} instance against the target * LinkProperties in {@code obj}. Two LinkPropertieses are equal if * all their fields are equal in values. @@ -1537,7 +1656,9 @@ public final class LinkProperties implements Parcelable { && isIdenticalMtu(target) && isIdenticalTcpBufferSizes(target) && isIdenticalNat64Prefix(target) - && isIdenticalWakeOnLan(target); + && isIdenticalWakeOnLan(target) + && isIdenticalCaptivePortalApiUrl(target) + && isIdenticalCaptivePortalData(target); } /** @@ -1655,7 +1776,8 @@ public final class LinkProperties implements Parcelable { + mPcscfs.size() * 67 + ((null == mPrivateDnsServerName) ? 0 : mPrivateDnsServerName.hashCode()) + Objects.hash(mNat64Prefix) - + (mWakeOnLanSupported ? 71 : 0); + + (mWakeOnLanSupported ? 71 : 0) + + Objects.hash(mCaptivePortalApiUrl, mCaptivePortalData); } /** @@ -1694,6 +1816,8 @@ public final class LinkProperties implements Parcelable { dest.writeList(stackedLinks); dest.writeBoolean(mWakeOnLanSupported); + dest.writeParcelable(mParcelSensitiveFields ? mCaptivePortalApiUrl : null, 0); + dest.writeParcelable(mParcelSensitiveFields ? mCaptivePortalData : null, 0); } private static void writeAddresses(@NonNull Parcel dest, @NonNull List<InetAddress> list) { @@ -1785,6 +1909,9 @@ public final class LinkProperties implements Parcelable { netProp.addStackedLink(stackedLink); } netProp.setWakeOnLanSupported(in.readBoolean()); + + netProp.setCaptivePortalApiUrl(in.readParcelable(null)); + netProp.setCaptivePortalData(in.readParcelable(null)); return netProp; } diff --git a/core/java/android/net/Uri.aidl b/core/java/android/net/Uri.aidl index 6bd3be5e041b..b85f63bbcdd2 100644 --- a/core/java/android/net/Uri.aidl +++ b/core/java/android/net/Uri.aidl @@ -16,4 +16,4 @@ package android.net; -parcelable Uri; +@JavaOnlyStableParcelable parcelable Uri; diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index da42edda20d3..783ab4ba5866 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -1573,48 +1573,49 @@ public class ConnectivityService extends IConnectivityManager.Stub enforceAccessPermission(); final int uid = Binder.getCallingUid(); NetworkState state = getUnfilteredActiveNetworkState(uid); - return state.linkProperties; + if (state.linkProperties == null) return null; + return linkPropertiesRestrictedForCallerPermissions(state.linkProperties, + Binder.getCallingPid(), uid); } @Override public LinkProperties getLinkPropertiesForType(int networkType) { enforceAccessPermission(); NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); - if (nai != null) { - synchronized (nai) { - return new LinkProperties(nai.linkProperties); - } - } - return null; + final LinkProperties lp = getLinkProperties(nai); + if (lp == null) return null; + return linkPropertiesRestrictedForCallerPermissions( + lp, Binder.getCallingPid(), Binder.getCallingUid()); } // TODO - this should be ALL networks @Override public LinkProperties getLinkProperties(Network network) { enforceAccessPermission(); - return getLinkProperties(getNetworkAgentInfoForNetwork(network)); + final LinkProperties lp = getLinkProperties(getNetworkAgentInfoForNetwork(network)); + if (lp == null) return null; + return linkPropertiesRestrictedForCallerPermissions( + lp, Binder.getCallingPid(), Binder.getCallingUid()); } - private LinkProperties getLinkProperties(NetworkAgentInfo nai) { + @Nullable + private LinkProperties getLinkProperties(@Nullable NetworkAgentInfo nai) { if (nai == null) { return null; } synchronized (nai) { - return new LinkProperties(nai.linkProperties); + return nai.linkProperties; } } private NetworkCapabilities getNetworkCapabilitiesInternal(NetworkAgentInfo nai) { - if (nai != null) { - synchronized (nai) { - if (nai.networkCapabilities != null) { - return networkCapabilitiesRestrictedForCallerPermissions( - nai.networkCapabilities, - Binder.getCallingPid(), Binder.getCallingUid()); - } - } + if (nai == null) return null; + synchronized (nai) { + if (nai.networkCapabilities == null) return null; + return networkCapabilitiesRestrictedForCallerPermissions( + nai.networkCapabilities, + Binder.getCallingPid(), Binder.getCallingUid()); } - return null; } @Override @@ -1636,6 +1637,29 @@ public class ConnectivityService extends IConnectivityManager.Stub return newNc; } + private LinkProperties linkPropertiesRestrictedForCallerPermissions( + LinkProperties lp, int callerPid, int callerUid) { + if (lp == null) return new LinkProperties(); + + // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls. + final boolean needsSanitization = + (lp.getCaptivePortalApiUrl() != null || lp.getCaptivePortalData() != null); + if (!needsSanitization) { + return new LinkProperties(lp); + } + + if (checkSettingsPermission(callerPid, callerUid)) { + return lp.makeSensitiveFieldsParcelingCopy(); + } + + final LinkProperties newLp = new LinkProperties(lp); + // Sensitive fields would not be parceled anyway, but sanitize for consistency before the + // object gets parceled. + newLp.setCaptivePortalApiUrl(null); + newLp.setCaptivePortalData(null); + return newLp; + } + private void restrictRequestUidsForCaller(NetworkCapabilities nc) { if (!checkSettingsPermission()) { nc.setSingleUid(Binder.getCallingUid()); @@ -6131,7 +6155,8 @@ public class ConnectivityService extends IConnectivityManager.Stub case ConnectivityManager.CALLBACK_AVAILABLE: { putParcelable(bundle, networkCapabilitiesRestrictedForCallerPermissions( networkAgent.networkCapabilities, nri.mPid, nri.mUid)); - putParcelable(bundle, new LinkProperties(networkAgent.linkProperties)); + putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions( + networkAgent.linkProperties, nri.mPid, nri.mUid)); // For this notification, arg1 contains the blocked status. msg.arg1 = arg1; break; @@ -6148,7 +6173,8 @@ public class ConnectivityService extends IConnectivityManager.Stub break; } case ConnectivityManager.CALLBACK_IP_CHANGED: { - putParcelable(bundle, new LinkProperties(networkAgent.linkProperties)); + putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions( + networkAgent.linkProperties, nri.mPid, nri.mUid)); break; } case ConnectivityManager.CALLBACK_BLK_CHANGED: { diff --git a/tests/net/common/java/android/net/LinkPropertiesTest.java b/tests/net/common/java/android/net/LinkPropertiesTest.java index a7328acb73b5..3f311c951c7e 100644 --- a/tests/net/common/java/android/net/LinkPropertiesTest.java +++ b/tests/net/common/java/android/net/LinkPropertiesTest.java @@ -75,6 +75,9 @@ public class LinkPropertiesTest { private static final LinkAddress LINKADDRV4 = new LinkAddress(ADDRV4, 32); private static final LinkAddress LINKADDRV6 = new LinkAddress(ADDRV6, 128); private static final LinkAddress LINKADDRV6LINKLOCAL = new LinkAddress("fe80::1/64"); + private static final Uri CAPPORT_API_URL = Uri.parse("https://test.example.com/capportapi"); + private static final CaptivePortalData CAPPORT_DATA = new CaptivePortalData.Builder() + .setVenueInfoUrl(Uri.parse("https://test.example.com/venue")).build(); private static InetAddress address(String addrString) { return InetAddresses.parseNumericAddress(addrString); @@ -101,6 +104,8 @@ public class LinkPropertiesTest { assertFalse(lp.isIpv6Provisioned()); assertFalse(lp.isPrivateDnsActive()); assertFalse(lp.isWakeOnLanSupported()); + assertNull(lp.getCaptivePortalApiUrl()); + assertNull(lp.getCaptivePortalData()); } private LinkProperties makeTestObject() { @@ -124,6 +129,8 @@ public class LinkPropertiesTest { lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96")); lp.setDhcpServerAddress(DHCPSERVER); lp.setWakeOnLanSupported(true); + lp.setCaptivePortalApiUrl(CAPPORT_API_URL); + lp.setCaptivePortalData(CAPPORT_DATA); return lp; } @@ -165,6 +172,12 @@ public class LinkPropertiesTest { assertTrue(source.isIdenticalWakeOnLan(target)); assertTrue(target.isIdenticalWakeOnLan(source)); + assertTrue(source.isIdenticalCaptivePortalApiUrl(target)); + assertTrue(target.isIdenticalCaptivePortalApiUrl(source)); + + assertTrue(source.isIdenticalCaptivePortalData(target)); + assertTrue(target.isIdenticalCaptivePortalData(source)); + // Check result of equals(). assertTrue(source.equals(target)); assertTrue(target.equals(source)); @@ -963,6 +976,8 @@ public class LinkPropertiesTest { source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96")); source.setWakeOnLanSupported(true); + source.setCaptivePortalApiUrl(CAPPORT_API_URL); + source.setCaptivePortalData(CAPPORT_DATA); source.setDhcpServerAddress((Inet4Address) GATEWAY1); @@ -970,7 +985,13 @@ public class LinkPropertiesTest { stacked.setInterfaceName("test-stacked"); source.addStackedLink(stacked); - assertParcelSane(source, 16 /* fieldCount */); + assertParcelSane(source.makeSensitiveFieldsParcelingCopy(), 18 /* fieldCount */); + + // Verify that without using a sensitiveFieldsParcelingCopy, sensitive fields are cleared. + final LinkProperties sanitized = new LinkProperties(source); + sanitized.setCaptivePortalApiUrl(null); + sanitized.setCaptivePortalData(null); + assertEquals(sanitized, parcelingRoundTrip(source)); } @Test @@ -1113,4 +1134,22 @@ public class LinkPropertiesTest { lp.clear(); assertFalse(lp.isWakeOnLanSupported()); } + + @Test + public void testCaptivePortalApiUrl() { + final LinkProperties lp = makeTestObject(); + assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl()); + + lp.clear(); + assertNull(lp.getCaptivePortalApiUrl()); + } + + @Test + public void testCaptivePortalData() { + final LinkProperties lp = makeTestObject(); + assertEquals(CAPPORT_DATA, lp.getCaptivePortalData()); + + lp.clear(); + assertNull(lp.getCaptivePortalData()); + } } diff --git a/tests/net/java/android/net/CaptivePortalDataTest.kt b/tests/net/java/android/net/CaptivePortalDataTest.kt new file mode 100644 index 000000000000..00714382684f --- /dev/null +++ b/tests/net/java/android/net/CaptivePortalDataTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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 android.net + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CaptivePortalDataTest { + private val data = CaptivePortalData.Builder() + .setRefreshTime(123L) + .setUserPortalUrl(Uri.parse("https://portal.example.com/test")) + .setVenueInfoUrl(Uri.parse("https://venue.example.com/test")) + .setSessionExtendable(true) + .setBytesRemaining(456L) + .setExpiryTime(789L) + .setCaptive(true) + .build() + + private fun makeBuilder() = CaptivePortalData.Builder(data) + + @Test + fun testParcelUnparcel() { + assertParcelSane(data, fieldCount = 7) + + assertParcelingIsLossless(makeBuilder().setUserPortalUrl(null).build()) + assertParcelingIsLossless(makeBuilder().setVenueInfoUrl(null).build()) + } + + @Test + fun testEquals() { + assertEquals(data, makeBuilder().build()) + + assertNotEqualsAfterChange { it.setRefreshTime(456L) } + assertNotEqualsAfterChange { it.setUserPortalUrl(Uri.parse("https://example.com/")) } + assertNotEqualsAfterChange { it.setUserPortalUrl(null) } + assertNotEqualsAfterChange { it.setVenueInfoUrl(Uri.parse("https://example.com/")) } + assertNotEqualsAfterChange { it.setVenueInfoUrl(null) } + assertNotEqualsAfterChange { it.setSessionExtendable(false) } + assertNotEqualsAfterChange { it.setBytesRemaining(789L) } + assertNotEqualsAfterChange { it.setExpiryTime(12L) } + assertNotEqualsAfterChange { it.setCaptive(false) } + } + + private fun CaptivePortalData.mutate(mutator: (CaptivePortalData.Builder) -> Unit) = + CaptivePortalData.Builder(this).apply { mutator(this) }.build() + + private fun assertNotEqualsAfterChange(mutator: (CaptivePortalData.Builder) -> Unit) { + assertNotEquals(data, data.mutate(mutator)) + } +}
\ No newline at end of file diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index 1901a1db633b..783f8d177f4a 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -21,6 +21,8 @@ import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION_SUPL; @@ -114,6 +116,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import android.Manifest; import android.annotation.NonNull; import android.app.AlarmManager; import android.app.NotificationManager; @@ -129,6 +132,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.content.res.Resources; +import android.net.CaptivePortalData; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.ConnectivityManager.PacketKeepalive; @@ -165,6 +169,7 @@ import android.net.ResolverParamsParcel; import android.net.RouteInfo; import android.net.SocketKeepalive; import android.net.UidRange; +import android.net.Uri; import android.net.metrics.IpConnectivityLog; import android.net.shared.NetworkMonitorUtils; import android.net.shared.PrivateDnsConfig; @@ -243,8 +248,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -347,6 +354,8 @@ public class ConnectivityServiceTest { @Spy private Resources mResources; private final LinkedBlockingQueue<Intent> mStartedActivities = new LinkedBlockingQueue<>(); + // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant + private final HashMap<String, Integer> mMockedPermissions = new HashMap<>(); MockContext(Context base, ContentProvider settingsProvider) { super(base); @@ -417,13 +426,39 @@ public class ConnectivityServiceTest { } @Override + public int checkPermission(String permission, int pid, int uid) { + final Integer granted = mMockedPermissions.get(permission); + if (granted == null) { + // All non-mocked permissions should be held by the test or unnecessary: check as + // normal to make sure the code does not rely on unexpected permissions. + return super.checkPermission(permission, pid, uid); + } + return granted; + } + + @Override public void enforceCallingOrSelfPermission(String permission, String message) { - // The mainline permission can only be held if signed with the network stack certificate - // Skip testing for this permission. - if (NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK.equals(permission)) return; - // All other permissions should be held by the test or unnecessary: check as normal to - // make sure the code does not rely on unexpected permissions. - super.enforceCallingOrSelfPermission(permission, message); + final Integer granted = mMockedPermissions.get(permission); + if (granted == null) { + super.enforceCallingOrSelfPermission(permission, message); + return; + } + + if (!granted.equals(PERMISSION_GRANTED)) { + throw new SecurityException("[Test] permission denied: " + permission); + } + } + + /** + * Mock checks for the specified permission, and have them behave as per {@code granted}. + * + * <p>Passing null reverts to default behavior, which does a real permission check on the + * test package. + * @param granted One of {@link PackageManager#PERMISSION_GRANTED} or + * {@link PackageManager#PERMISSION_DENIED}. + */ + public void setPermission(String permission, Integer granted) { + mMockedPermissions.put(permission, granted); } @Override @@ -1750,6 +1785,66 @@ public class ConnectivityServiceTest { assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); } + private void doNetworkCallbacksSanitizationTest(boolean sanitized) throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(wifiRequest, callback); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + final LinkProperties newLp = new LinkProperties(); + final Uri capportUrl = Uri.parse("https://capport.example.com/api"); + final CaptivePortalData capportData = new CaptivePortalData.Builder() + .setCaptive(true).build(); + newLp.setCaptivePortalApiUrl(capportUrl); + newLp.setCaptivePortalData(capportData); + mWiFiNetworkAgent.sendLinkProperties(newLp); + + final Uri expectedCapportUrl = sanitized ? null : capportUrl; + final CaptivePortalData expectedCapportData = sanitized ? null : capportData; + callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl()) + && Objects.equals(expectedCapportData, lp.getCaptivePortalData())); + defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl()) + && Objects.equals(expectedCapportData, lp.getCaptivePortalData())); + + final LinkProperties lp = mCm.getLinkProperties(mWiFiNetworkAgent.getNetwork()); + assertEquals(expectedCapportUrl, lp.getCaptivePortalApiUrl()); + assertEquals(expectedCapportData, lp.getCaptivePortalData()); + } + + @Test + public void networkCallbacksSanitizationTest_Sanitize() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(Manifest.permission.NETWORK_SETTINGS, + PERMISSION_DENIED); + doNetworkCallbacksSanitizationTest(true /* sanitized */); + } + + @Test + public void networkCallbacksSanitizationTest_NoSanitize_NetworkStack() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_GRANTED); + mServiceContext.setPermission(Manifest.permission.NETWORK_SETTINGS, PERMISSION_DENIED); + doNetworkCallbacksSanitizationTest(false /* sanitized */); + } + + @Test + public void networkCallbacksSanitizationTest_NoSanitize_Settings() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(Manifest.permission.NETWORK_SETTINGS, PERMISSION_GRANTED); + doNetworkCallbacksSanitizationTest(false /* sanitized */); + } + @Test public void testMultipleLingering() throws Exception { // This test would be flaky with the default 120ms timer: that is short enough that @@ -2628,6 +2723,8 @@ public class ConnectivityServiceTest { final String testKey = "testkey"; final String testValue = "testvalue"; testBundle.putString(testKey, testValue); + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_GRANTED); mCm.startCaptivePortalApp(wifiNetwork, testBundle); final Intent signInIntent = mServiceContext.expectStartActivityIntent(TIMEOUT_MS); assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction()); |