Loading core/java/android/net/ipmemorystore/NetworkAttributes.java +6 −0 Original line number Diff line number Diff line Loading @@ -252,6 +252,12 @@ public class NetworkAttributes { } } /** @hide */ public boolean isEmpty() { return (null == assignedV4Address) && (null == groupHint) && (null == dnsAddresses) && (null == mtu); } @Override public boolean equals(@Nullable final Object o) { if (!(o instanceof NetworkAttributes)) return false; Loading services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java +177 −45 Original line number Diff line number Diff line Loading @@ -21,9 +21,12 @@ import android.annotation.Nullable; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteCursorDriver; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQuery; import android.net.NetworkUtils; import android.net.ipmemorystore.NetworkAttributes; import android.net.ipmemorystore.Status; Loading @@ -35,6 +38,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; /** * Encapsulating class for using the SQLite database backing the memory store. Loading @@ -46,6 +50,9 @@ import java.util.List; */ public class IpMemoryStoreDatabase { private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName(); // A pair of NetworkAttributes objects is group-close if the confidence that they are // the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse. private static final float GROUPCLOSE_CONFIDENCE = 0.5f; /** * Contract class for the Network Attributes table. Loading Loading @@ -187,15 +194,10 @@ public class IpMemoryStoreDatabase { return addresses; } // Convert a NetworkAttributes object to content values to store them in a table compliant // with the contract defined in NetworkAttributesContract. @NonNull private static ContentValues toContentValues(@NonNull final String key, @Nullable final NetworkAttributes attributes, final long expiry) { private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) { final ContentValues values = new ContentValues(); values.put(NetworkAttributesContract.COLNAME_L2KEY, key); values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry); if (null != attributes) { if (null == attributes) return values; if (null != attributes.assignedV4Address) { values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address)); Loading @@ -210,7 +212,17 @@ public class IpMemoryStoreDatabase { if (null != attributes.mtu) { values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu); } return values; } // Convert a NetworkAttributes object to content values to store them in a table compliant // with the contract defined in NetworkAttributesContract. @NonNull private static ContentValues toContentValues(@NonNull final String key, @Nullable final NetworkAttributes attributes, final long expiry) { final ContentValues values = toContentValues(attributes); values.put(NetworkAttributesContract.COLNAME_L2KEY, key); values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry); return values; } Loading @@ -228,6 +240,32 @@ public class IpMemoryStoreDatabase { return values; } @Nullable private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) { // Make sure the data hasn't expired final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L); if (expiry < System.currentTimeMillis()) return null; final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); final int assignedV4AddressInt = getInt(cursor, NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0); final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT); final byte[] dnsAddressesBlob = getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES); final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1); if (0 != assignedV4AddressInt) { builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt)); } builder.setGroupHint(groupHint); if (null != dnsAddressesBlob) { builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob)); } if (mtu >= 0) { builder.setMtu(mtu); } return builder.build(); } private static final String[] EXPIRY_COLUMN = new String[] { NetworkAttributesContract.COLNAME_EXPIRYDATE }; Loading Loading @@ -313,32 +351,9 @@ public class IpMemoryStoreDatabase { // result here. 0 results means the key was not found. if (cursor.getCount() != 1) return null; cursor.moveToFirst(); // Make sure the data hasn't expired final long expiry = cursor.getLong( cursor.getColumnIndexOrThrow(NetworkAttributesContract.COLNAME_EXPIRYDATE)); if (expiry < System.currentTimeMillis()) return null; final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); final int assignedV4AddressInt = getInt(cursor, NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0); final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT); final byte[] dnsAddressesBlob = getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES); final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1); final NetworkAttributes attributes = readNetworkAttributesLine(cursor); cursor.close(); if (0 != assignedV4AddressInt) { builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt)); } builder.setGroupHint(groupHint); if (null != dnsAddressesBlob) { builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob)); } if (mtu >= 0) { builder.setMtu(mtu); } return builder.build(); return attributes; } private static final String[] DATA_COLUMN = new String[] { Loading @@ -365,17 +380,134 @@ public class IpMemoryStoreDatabase { return result; } /** * The following is a horrible hack that is necessary because the Android SQLite API does not * have a way to query a binary blob. This, almost certainly, is an overlook. * * The Android SQLite API has two family of methods : one for query that returns data, and * one for more general SQL statements that can execute any statement but may not return * anything. All the query methods, however, take only String[] for the arguments. * * In principle it is simple to write a function that will encode the binary blob in the * way SQLite expects it. However, because the API forces the argument to be coerced into a * String, the SQLiteQuery object generated by the default query methods will bind all * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types, * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will * be sanitized, changing the contents of the field, and the query will fail to match the * blob. * * As far as I can tell, there are two possible ways around this problem. The first one * is to put the data in the query string and eschew it being an argument. This would * require doing the sanitizing by hand. The other is to call bindBlob directly on the * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out * sanitizing, but also will do the right thing if the underlying format ever changes. * * But none of the methods that take an SQLiteQuery object can return data ; this *must* * be called with SQLiteDatabase#query. This object is not accessible from outside. * However, there is a #query version that accepts a CursorFactory and this is pretty * straightforward to implement as all the arguments are coming in and the SQLiteCursor * class is public API. * With this, it's possible to intercept the SQLiteQuery object, and assuming the args * are available, to bind them directly and work around the API's oblivious coercion into * Strings. * * This is really sad, but I don't see another way of having this work than this or the * hand-rolled sanitizing, and this is the lesser evil. */ private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory { @NonNull private final ArrayList<Object> mArgs; CustomCursorFactory(@NonNull final ArrayList<Object> args) { mArgs = args; } @Override public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery, final String editTable, final SQLiteQuery query) { int index = 1; // bind is 1-indexed for (final Object arg : mArgs) { if (arg instanceof String) { query.bindString(index++, (String) arg); } else if (arg instanceof Long) { query.bindLong(index++, (Long) arg); } else if (arg instanceof Integer) { query.bindLong(index++, Long.valueOf((Integer) arg)); } else if (arg instanceof byte[]) { query.bindBlob(index++, (byte[]) arg); } else { throw new IllegalStateException("Unsupported type CustomCursorFactory " + arg.getClass().toString()); } } return new SQLiteCursor(masterQuery, editTable, query); } } // Returns the l2key of the closest match, if and only if it matches // closely enough (as determined by group-closeness). @Nullable static String findClosestAttributes(@NonNull final SQLiteDatabase db, @NonNull final NetworkAttributes attr) { if (attr.isEmpty()) return null; final ContentValues values = toContentValues(attr); // Build the selection and args. To cut down on the number of lines to search, limit // the search to those with at least one argument equals to the requested attributes. // This works only because null attributes match only will not result in group-closeness. final StringJoiner sj = new StringJoiner(" OR "); final ArrayList<Object> args = new ArrayList<>(); args.add(System.currentTimeMillis()); for (final String field : values.keySet()) { sj.add(field + " = ?"); args.add(values.get(field)); } final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND (" + sj.toString() + ")"; final Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args), false, // distinct NetworkAttributesContract.TABLENAME, null, // columns, null means everything selection, // selection null, // selectionArgs, horrendously passed to the cursor factory instead null, // groupBy null, // having null, // orderBy null); // limit if (cursor.getCount() <= 0) return null; cursor.moveToFirst(); String bestKey = null; float bestMatchConfidence = GROUPCLOSE_CONFIDENCE; // Never return a match worse than this. while (!cursor.isAfterLast()) { final NetworkAttributes read = readNetworkAttributesLine(cursor); final float confidence = read.getNetworkGroupSamenessConfidence(attr); if (confidence > bestMatchConfidence) { bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY); bestMatchConfidence = confidence; } cursor.moveToNext(); } cursor.close(); return bestKey; } // Helper methods static String getString(final Cursor cursor, final String columnName) { private static String getString(final Cursor cursor, final String columnName) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getString(columnIndex) : null; } static byte[] getBlob(final Cursor cursor, final String columnName) { private static byte[] getBlob(final Cursor cursor, final String columnName) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null; } static int getInt(final Cursor cursor, final String columnName, final int defaultValue) { private static int getInt(final Cursor cursor, final String columnName, final int defaultValue) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue; } private static long getLong(final Cursor cursor, final String columnName, final long defaultValue) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue; } } services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java +20 −3 Original line number Diff line number Diff line Loading @@ -250,9 +250,26 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { * Through the listener, returns the L2 key if one matched, or null. */ @Override public void findL2Key(@NonNull final NetworkAttributesParcelable attributes, @NonNull final IOnL2KeyResponseListener listener) { // TODO : implement this. public void findL2Key(@Nullable final NetworkAttributesParcelable attributes, @Nullable final IOnL2KeyResponseListener listener) { if (null == listener) return; mExecutor.execute(() -> { try { if (null == attributes) { listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); return; } if (null == mDb) { listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); return; } final String key = IpMemoryStoreDatabase.findClosestAttributes(mDb, new NetworkAttributes(attributes)); listener.onL2KeyResponse(makeStatus(SUCCESS), key); } catch (final RemoteException e) { // Client at the other end died } }); } /** Loading tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java +120 −13 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import static org.mockito.Mockito.doReturn; import android.content.Context; import android.net.ipmemorystore.Blob; import android.net.ipmemorystore.IOnBlobRetrievedListener; import android.net.ipmemorystore.IOnL2KeyResponseListener; import android.net.ipmemorystore.IOnNetworkAttributesRetrieved; import android.net.ipmemorystore.IOnSameNetworkResponseListener; import android.net.ipmemorystore.IOnStatusListener; Loading Loading @@ -67,7 +68,14 @@ public class IpMemoryStoreServiceTest { private static final String TEST_CLIENT_ID = "testClientId"; private static final String TEST_DATA_NAME = "testData"; private static final String[] FAKE_KEYS = { "fakeKey1", "fakeKey2", "fakeKey3", "fakeKey4" }; private static final int FAKE_KEY_COUNT = 20; private static final String[] FAKE_KEYS; static { FAKE_KEYS = new String[FAKE_KEY_COUNT]; for (int i = 0; i < FAKE_KEYS.length; ++i) { FAKE_KEYS[i] = "fakeKey" + i; } } @Mock private Context mMockContext; Loading Loading @@ -170,6 +178,25 @@ public class IpMemoryStoreServiceTest { }; } /** Helper method to make an IOnL2KeyResponseListener */ private interface OnL2KeyResponseListener { void onL2KeyResponse(Status status, String key); } private IOnL2KeyResponseListener onL2KeyResponse(final OnL2KeyResponseListener functor) { return new IOnL2KeyResponseListener() { @Override public void onL2KeyResponse(final StatusParcelable status, final String key) throws RemoteException { functor.onL2KeyResponse(new Status(status), key); } @Override public IBinder asBinder() { return null; } }; } // Helper method to factorize some boilerplate private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) { final CountDownLatch latch = new CountDownLatch(1); Loading @@ -195,12 +222,9 @@ public class IpMemoryStoreServiceTest { } @Test public void testNetworkAttributes() { public void testNetworkAttributes() throws UnknownHostException { final NetworkAttributes.Builder na = new NetworkAttributes.Builder(); try { na.setAssignedV4Address( (Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4})); } catch (UnknownHostException e) { /* Can't happen */ } na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setGroupHint("hint1"); na.setMtu(219); final String l2Key = FAKE_KEYS[0]; Loading @@ -218,10 +242,8 @@ public class IpMemoryStoreServiceTest { }))); final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder(); try { na.setDnsAddresses(Arrays.asList( new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")})); } catch (UnknownHostException e) { /* Still can't happen */ } final NetworkAttributes attributes2 = na2.build(); storeAttributes("Did not complete storing attributes 2", l2Key, attributes2); Loading Loading @@ -333,8 +355,93 @@ public class IpMemoryStoreServiceTest { } @Test public void testFindL2Key() { // TODO : implement this public void testFindL2Key() throws UnknownHostException { final NetworkAttributes.Builder na = new NetworkAttributes.Builder(); na.setGroupHint("hint0"); storeAttributes(FAKE_KEYS[0], na.build()); na.setDnsAddresses(Arrays.asList( new InetAddress[] {Inet6Address.getByName("8D56:9AF1::08EE:20F1")})); na.setMtu(219); storeAttributes(FAKE_KEYS[1], na.build()); na.setMtu(null); na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setDnsAddresses(Arrays.asList( new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")})); na.setGroupHint("hint1"); storeAttributes(FAKE_KEYS[2], na.build()); na.setMtu(219); storeAttributes(FAKE_KEYS[3], na.build()); na.setMtu(240); storeAttributes(FAKE_KEYS[4], na.build()); na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("5.6.7.8")); storeAttributes(FAKE_KEYS[5], na.build()); // Matches key 5 exactly doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[5], key); }))); // MTU matches key 4 but v4 address matches key 5. The latter is stronger. na.setMtu(240); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[5], key); }))); // Closest to key 3 (indeed, identical) na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setMtu(219); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[3], key); }))); // Group hint alone must not be strong enough to override the rest na.setGroupHint("hint0"); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[3], key); }))); // Still closest to key 3, though confidence is lower na.setGroupHint("hint1"); na.setDnsAddresses(null); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[3], key); }))); // But changing the MTU makes this closer to key 4 na.setMtu(240); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[4], key); }))); // MTU alone not strong enough to make this group-close na.setGroupHint(null); na.setDnsAddresses(null); na.setAssignedV4Address(null); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertNull(key); }))); } private void assertNetworksSameness(final String key1, final String key2, final int sameness) { Loading @@ -349,7 +456,7 @@ public class IpMemoryStoreServiceTest { @Test public void testIsSameNetwork() throws UnknownHostException { final NetworkAttributes.Builder na = new NetworkAttributes.Builder(); na.setAssignedV4Address((Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4})); na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setGroupHint("hint1"); na.setMtu(219); na.setDnsAddresses(Arrays.asList(Inet6Address.getByName("0A1C:2E40:480A::1CA6"))); Loading Loading
core/java/android/net/ipmemorystore/NetworkAttributes.java +6 −0 Original line number Diff line number Diff line Loading @@ -252,6 +252,12 @@ public class NetworkAttributes { } } /** @hide */ public boolean isEmpty() { return (null == assignedV4Address) && (null == groupHint) && (null == dnsAddresses) && (null == mtu); } @Override public boolean equals(@Nullable final Object o) { if (!(o instanceof NetworkAttributes)) return false; Loading
services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreDatabase.java +177 −45 Original line number Diff line number Diff line Loading @@ -21,9 +21,12 @@ import android.annotation.Nullable; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteCursor; import android.database.sqlite.SQLiteCursorDriver; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQuery; import android.net.NetworkUtils; import android.net.ipmemorystore.NetworkAttributes; import android.net.ipmemorystore.Status; Loading @@ -35,6 +38,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; /** * Encapsulating class for using the SQLite database backing the memory store. Loading @@ -46,6 +50,9 @@ import java.util.List; */ public class IpMemoryStoreDatabase { private static final String TAG = IpMemoryStoreDatabase.class.getSimpleName(); // A pair of NetworkAttributes objects is group-close if the confidence that they are // the same is above this cutoff. See NetworkAttributes and SameL3NetworkResponse. private static final float GROUPCLOSE_CONFIDENCE = 0.5f; /** * Contract class for the Network Attributes table. Loading Loading @@ -187,15 +194,10 @@ public class IpMemoryStoreDatabase { return addresses; } // Convert a NetworkAttributes object to content values to store them in a table compliant // with the contract defined in NetworkAttributesContract. @NonNull private static ContentValues toContentValues(@NonNull final String key, @Nullable final NetworkAttributes attributes, final long expiry) { private static ContentValues toContentValues(@Nullable final NetworkAttributes attributes) { final ContentValues values = new ContentValues(); values.put(NetworkAttributesContract.COLNAME_L2KEY, key); values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry); if (null != attributes) { if (null == attributes) return values; if (null != attributes.assignedV4Address) { values.put(NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, NetworkUtils.inet4AddressToIntHTH(attributes.assignedV4Address)); Loading @@ -210,7 +212,17 @@ public class IpMemoryStoreDatabase { if (null != attributes.mtu) { values.put(NetworkAttributesContract.COLNAME_MTU, attributes.mtu); } return values; } // Convert a NetworkAttributes object to content values to store them in a table compliant // with the contract defined in NetworkAttributesContract. @NonNull private static ContentValues toContentValues(@NonNull final String key, @Nullable final NetworkAttributes attributes, final long expiry) { final ContentValues values = toContentValues(attributes); values.put(NetworkAttributesContract.COLNAME_L2KEY, key); values.put(NetworkAttributesContract.COLNAME_EXPIRYDATE, expiry); return values; } Loading @@ -228,6 +240,32 @@ public class IpMemoryStoreDatabase { return values; } @Nullable private static NetworkAttributes readNetworkAttributesLine(@NonNull final Cursor cursor) { // Make sure the data hasn't expired final long expiry = getLong(cursor, NetworkAttributesContract.COLNAME_EXPIRYDATE, -1L); if (expiry < System.currentTimeMillis()) return null; final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); final int assignedV4AddressInt = getInt(cursor, NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0); final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT); final byte[] dnsAddressesBlob = getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES); final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1); if (0 != assignedV4AddressInt) { builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt)); } builder.setGroupHint(groupHint); if (null != dnsAddressesBlob) { builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob)); } if (mtu >= 0) { builder.setMtu(mtu); } return builder.build(); } private static final String[] EXPIRY_COLUMN = new String[] { NetworkAttributesContract.COLNAME_EXPIRYDATE }; Loading Loading @@ -313,32 +351,9 @@ public class IpMemoryStoreDatabase { // result here. 0 results means the key was not found. if (cursor.getCount() != 1) return null; cursor.moveToFirst(); // Make sure the data hasn't expired final long expiry = cursor.getLong( cursor.getColumnIndexOrThrow(NetworkAttributesContract.COLNAME_EXPIRYDATE)); if (expiry < System.currentTimeMillis()) return null; final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); final int assignedV4AddressInt = getInt(cursor, NetworkAttributesContract.COLNAME_ASSIGNEDV4ADDRESS, 0); final String groupHint = getString(cursor, NetworkAttributesContract.COLNAME_GROUPHINT); final byte[] dnsAddressesBlob = getBlob(cursor, NetworkAttributesContract.COLNAME_DNSADDRESSES); final int mtu = getInt(cursor, NetworkAttributesContract.COLNAME_MTU, -1); final NetworkAttributes attributes = readNetworkAttributesLine(cursor); cursor.close(); if (0 != assignedV4AddressInt) { builder.setAssignedV4Address(NetworkUtils.intToInet4AddressHTH(assignedV4AddressInt)); } builder.setGroupHint(groupHint); if (null != dnsAddressesBlob) { builder.setDnsAddresses(decodeAddressList(dnsAddressesBlob)); } if (mtu >= 0) { builder.setMtu(mtu); } return builder.build(); return attributes; } private static final String[] DATA_COLUMN = new String[] { Loading @@ -365,17 +380,134 @@ public class IpMemoryStoreDatabase { return result; } /** * The following is a horrible hack that is necessary because the Android SQLite API does not * have a way to query a binary blob. This, almost certainly, is an overlook. * * The Android SQLite API has two family of methods : one for query that returns data, and * one for more general SQL statements that can execute any statement but may not return * anything. All the query methods, however, take only String[] for the arguments. * * In principle it is simple to write a function that will encode the binary blob in the * way SQLite expects it. However, because the API forces the argument to be coerced into a * String, the SQLiteQuery object generated by the default query methods will bind all * arguments as Strings and SQL will *sanitize* them. This works okay for numeric types, * but the format for blobs is x'<hex string>'. Note the presence of quotes, which will * be sanitized, changing the contents of the field, and the query will fail to match the * blob. * * As far as I can tell, there are two possible ways around this problem. The first one * is to put the data in the query string and eschew it being an argument. This would * require doing the sanitizing by hand. The other is to call bindBlob directly on the * generated SQLiteQuery object, which not only is a lot less dangerous than rolling out * sanitizing, but also will do the right thing if the underlying format ever changes. * * But none of the methods that take an SQLiteQuery object can return data ; this *must* * be called with SQLiteDatabase#query. This object is not accessible from outside. * However, there is a #query version that accepts a CursorFactory and this is pretty * straightforward to implement as all the arguments are coming in and the SQLiteCursor * class is public API. * With this, it's possible to intercept the SQLiteQuery object, and assuming the args * are available, to bind them directly and work around the API's oblivious coercion into * Strings. * * This is really sad, but I don't see another way of having this work than this or the * hand-rolled sanitizing, and this is the lesser evil. */ private static class CustomCursorFactory implements SQLiteDatabase.CursorFactory { @NonNull private final ArrayList<Object> mArgs; CustomCursorFactory(@NonNull final ArrayList<Object> args) { mArgs = args; } @Override public Cursor newCursor(final SQLiteDatabase db, final SQLiteCursorDriver masterQuery, final String editTable, final SQLiteQuery query) { int index = 1; // bind is 1-indexed for (final Object arg : mArgs) { if (arg instanceof String) { query.bindString(index++, (String) arg); } else if (arg instanceof Long) { query.bindLong(index++, (Long) arg); } else if (arg instanceof Integer) { query.bindLong(index++, Long.valueOf((Integer) arg)); } else if (arg instanceof byte[]) { query.bindBlob(index++, (byte[]) arg); } else { throw new IllegalStateException("Unsupported type CustomCursorFactory " + arg.getClass().toString()); } } return new SQLiteCursor(masterQuery, editTable, query); } } // Returns the l2key of the closest match, if and only if it matches // closely enough (as determined by group-closeness). @Nullable static String findClosestAttributes(@NonNull final SQLiteDatabase db, @NonNull final NetworkAttributes attr) { if (attr.isEmpty()) return null; final ContentValues values = toContentValues(attr); // Build the selection and args. To cut down on the number of lines to search, limit // the search to those with at least one argument equals to the requested attributes. // This works only because null attributes match only will not result in group-closeness. final StringJoiner sj = new StringJoiner(" OR "); final ArrayList<Object> args = new ArrayList<>(); args.add(System.currentTimeMillis()); for (final String field : values.keySet()) { sj.add(field + " = ?"); args.add(values.get(field)); } final String selection = NetworkAttributesContract.COLNAME_EXPIRYDATE + " > ? AND (" + sj.toString() + ")"; final Cursor cursor = db.queryWithFactory(new CustomCursorFactory(args), false, // distinct NetworkAttributesContract.TABLENAME, null, // columns, null means everything selection, // selection null, // selectionArgs, horrendously passed to the cursor factory instead null, // groupBy null, // having null, // orderBy null); // limit if (cursor.getCount() <= 0) return null; cursor.moveToFirst(); String bestKey = null; float bestMatchConfidence = GROUPCLOSE_CONFIDENCE; // Never return a match worse than this. while (!cursor.isAfterLast()) { final NetworkAttributes read = readNetworkAttributesLine(cursor); final float confidence = read.getNetworkGroupSamenessConfidence(attr); if (confidence > bestMatchConfidence) { bestKey = getString(cursor, NetworkAttributesContract.COLNAME_L2KEY); bestMatchConfidence = confidence; } cursor.moveToNext(); } cursor.close(); return bestKey; } // Helper methods static String getString(final Cursor cursor, final String columnName) { private static String getString(final Cursor cursor, final String columnName) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getString(columnIndex) : null; } static byte[] getBlob(final Cursor cursor, final String columnName) { private static byte[] getBlob(final Cursor cursor, final String columnName) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getBlob(columnIndex) : null; } static int getInt(final Cursor cursor, final String columnName, final int defaultValue) { private static int getInt(final Cursor cursor, final String columnName, final int defaultValue) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getInt(columnIndex) : defaultValue; } private static long getLong(final Cursor cursor, final String columnName, final long defaultValue) { final int columnIndex = cursor.getColumnIndex(columnName); return (columnIndex >= 0) ? cursor.getLong(columnIndex) : defaultValue; } }
services/ipmemorystore/java/com/android/server/net/ipmemorystore/IpMemoryStoreService.java +20 −3 Original line number Diff line number Diff line Loading @@ -250,9 +250,26 @@ public class IpMemoryStoreService extends IIpMemoryStore.Stub { * Through the listener, returns the L2 key if one matched, or null. */ @Override public void findL2Key(@NonNull final NetworkAttributesParcelable attributes, @NonNull final IOnL2KeyResponseListener listener) { // TODO : implement this. public void findL2Key(@Nullable final NetworkAttributesParcelable attributes, @Nullable final IOnL2KeyResponseListener listener) { if (null == listener) return; mExecutor.execute(() -> { try { if (null == attributes) { listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); return; } if (null == mDb) { listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); return; } final String key = IpMemoryStoreDatabase.findClosestAttributes(mDb, new NetworkAttributes(attributes)); listener.onL2KeyResponse(makeStatus(SUCCESS), key); } catch (final RemoteException e) { // Client at the other end died } }); } /** Loading
tests/net/java/com/android/server/net/ipmemorystore/IpMemoryStoreServiceTest.java +120 −13 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import static org.mockito.Mockito.doReturn; import android.content.Context; import android.net.ipmemorystore.Blob; import android.net.ipmemorystore.IOnBlobRetrievedListener; import android.net.ipmemorystore.IOnL2KeyResponseListener; import android.net.ipmemorystore.IOnNetworkAttributesRetrieved; import android.net.ipmemorystore.IOnSameNetworkResponseListener; import android.net.ipmemorystore.IOnStatusListener; Loading Loading @@ -67,7 +68,14 @@ public class IpMemoryStoreServiceTest { private static final String TEST_CLIENT_ID = "testClientId"; private static final String TEST_DATA_NAME = "testData"; private static final String[] FAKE_KEYS = { "fakeKey1", "fakeKey2", "fakeKey3", "fakeKey4" }; private static final int FAKE_KEY_COUNT = 20; private static final String[] FAKE_KEYS; static { FAKE_KEYS = new String[FAKE_KEY_COUNT]; for (int i = 0; i < FAKE_KEYS.length; ++i) { FAKE_KEYS[i] = "fakeKey" + i; } } @Mock private Context mMockContext; Loading Loading @@ -170,6 +178,25 @@ public class IpMemoryStoreServiceTest { }; } /** Helper method to make an IOnL2KeyResponseListener */ private interface OnL2KeyResponseListener { void onL2KeyResponse(Status status, String key); } private IOnL2KeyResponseListener onL2KeyResponse(final OnL2KeyResponseListener functor) { return new IOnL2KeyResponseListener() { @Override public void onL2KeyResponse(final StatusParcelable status, final String key) throws RemoteException { functor.onL2KeyResponse(new Status(status), key); } @Override public IBinder asBinder() { return null; } }; } // Helper method to factorize some boilerplate private void doLatched(final String timeoutMessage, final Consumer<CountDownLatch> functor) { final CountDownLatch latch = new CountDownLatch(1); Loading @@ -195,12 +222,9 @@ public class IpMemoryStoreServiceTest { } @Test public void testNetworkAttributes() { public void testNetworkAttributes() throws UnknownHostException { final NetworkAttributes.Builder na = new NetworkAttributes.Builder(); try { na.setAssignedV4Address( (Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4})); } catch (UnknownHostException e) { /* Can't happen */ } na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setGroupHint("hint1"); na.setMtu(219); final String l2Key = FAKE_KEYS[0]; Loading @@ -218,10 +242,8 @@ public class IpMemoryStoreServiceTest { }))); final NetworkAttributes.Builder na2 = new NetworkAttributes.Builder(); try { na.setDnsAddresses(Arrays.asList( new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")})); } catch (UnknownHostException e) { /* Still can't happen */ } final NetworkAttributes attributes2 = na2.build(); storeAttributes("Did not complete storing attributes 2", l2Key, attributes2); Loading Loading @@ -333,8 +355,93 @@ public class IpMemoryStoreServiceTest { } @Test public void testFindL2Key() { // TODO : implement this public void testFindL2Key() throws UnknownHostException { final NetworkAttributes.Builder na = new NetworkAttributes.Builder(); na.setGroupHint("hint0"); storeAttributes(FAKE_KEYS[0], na.build()); na.setDnsAddresses(Arrays.asList( new InetAddress[] {Inet6Address.getByName("8D56:9AF1::08EE:20F1")})); na.setMtu(219); storeAttributes(FAKE_KEYS[1], na.build()); na.setMtu(null); na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setDnsAddresses(Arrays.asList( new InetAddress[] {Inet6Address.getByName("0A1C:2E40:480A::1CA6")})); na.setGroupHint("hint1"); storeAttributes(FAKE_KEYS[2], na.build()); na.setMtu(219); storeAttributes(FAKE_KEYS[3], na.build()); na.setMtu(240); storeAttributes(FAKE_KEYS[4], na.build()); na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("5.6.7.8")); storeAttributes(FAKE_KEYS[5], na.build()); // Matches key 5 exactly doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[5], key); }))); // MTU matches key 4 but v4 address matches key 5. The latter is stronger. na.setMtu(240); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[5], key); }))); // Closest to key 3 (indeed, identical) na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setMtu(219); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[3], key); }))); // Group hint alone must not be strong enough to override the rest na.setGroupHint("hint0"); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[3], key); }))); // Still closest to key 3, though confidence is lower na.setGroupHint("hint1"); na.setDnsAddresses(null); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[3], key); }))); // But changing the MTU makes this closer to key 4 na.setMtu(240); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertEquals(FAKE_KEYS[4], key); }))); // MTU alone not strong enough to make this group-close na.setGroupHint(null); na.setDnsAddresses(null); na.setAssignedV4Address(null); doLatched("Did not finish finding L2Key", latch -> mService.findL2Key(na.build().toParcelable(), onL2KeyResponse((status, key) -> { assertTrue("Retrieve network sameness not successful : " + status.resultCode, status.isSuccess()); assertNull(key); }))); } private void assertNetworksSameness(final String key1, final String key2, final int sameness) { Loading @@ -349,7 +456,7 @@ public class IpMemoryStoreServiceTest { @Test public void testIsSameNetwork() throws UnknownHostException { final NetworkAttributes.Builder na = new NetworkAttributes.Builder(); na.setAssignedV4Address((Inet4Address) Inet4Address.getByAddress(new byte[]{1, 2, 3, 4})); na.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); na.setGroupHint("hint1"); na.setMtu(219); na.setDnsAddresses(Arrays.asList(Inet6Address.getByName("0A1C:2E40:480A::1CA6"))); Loading