Commit 2f2c7513 authored by yusufo's avatar yusufo Committed by Commit bot

Add Digital Asset Links verification for postMessage API

Adds DigitalAssetLinks handler which queries digitalassetlinks.googleapis.com/
for verifying the relationship between an Android app and a web domain.
 This is added in a new component digital_asset_links since this API is
 a generic web API for checking Android-Web relationships. It may be
useful for iOS and ChromeOS as well. The component currently depends on
 base and net.

Then custom tabs uses this handler for verifying postMessage origin declared by
the client app when they send requestPostMessageChannel. This enabled
third party apps to use postMessage related APIs with secure and
verified origin declaration.

BUG=704975

Review-Url: https://codereview.chromium.org/2767333006
Cr-Commit-Position: refs/heads/master@{#467791}
parent 69a1f75e
...@@ -12,6 +12,7 @@ import android.content.pm.PackageManager; ...@@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsCallback; import android.support.customtabs.CustomTabsCallback;
import android.support.customtabs.CustomTabsService; import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsSessionToken; import android.support.customtabs.CustomTabsSessionToken;
...@@ -139,6 +140,7 @@ class ClientManager { ...@@ -139,6 +140,7 @@ class ClientManager {
packageName = getPackageName(context, uid); packageName = getPackageName(context, uid);
disconnectCallback = callback; disconnectCallback = callback;
this.postMessageHandler = postMessageHandler; this.postMessageHandler = postMessageHandler;
if (postMessageHandler != null) this.postMessageHandler.setPackageName(packageName);
this.mSpeculationMode = CustomTabsConnection.SpeculationParams.PRERENDER; this.mSpeculationMode = CustomTabsConnection.SpeculationParams.PRERENDER;
} }
...@@ -211,7 +213,7 @@ class ClientManager { ...@@ -211,7 +213,7 @@ class ClientManager {
* @return true for success. * @return true for success.
*/ */
public boolean newSession(CustomTabsSessionToken session, int uid, public boolean newSession(CustomTabsSessionToken session, int uid,
DisconnectCallback onDisconnect, PostMessageHandler postMessageHandler) { DisconnectCallback onDisconnect, @NonNull PostMessageHandler postMessageHandler) {
if (session == null) return false; if (session == null) return false;
SessionParams params = new SessionParams(mContext, uid, onDisconnect, postMessageHandler); SessionParams params = new SessionParams(mContext, uid, onDisconnect, postMessageHandler);
synchronized (this) { synchronized (this) {
...@@ -340,6 +342,26 @@ class ClientManager { ...@@ -340,6 +342,26 @@ class ClientManager {
params.postMessageHandler.initializeWithOrigin(origin); params.postMessageHandler.initializeWithOrigin(origin);
} }
/**
* See {@link PostMessageHandler#verifyAndInitializeWithOrigin(Uri)}.
*/
public synchronized void verifyAndInitializeWithPostMessageOriginForSession(
CustomTabsSessionToken session, Uri origin) {
SessionParams params = mSessionParams.get(session);
if (params == null) return;
params.postMessageHandler.verifyAndInitializeWithOrigin(origin);
}
/**
* @return The postMessage origin for the given session.
*/
@VisibleForTesting
synchronized Uri getPostMessageOriginForSessionForTesting(CustomTabsSessionToken session) {
SessionParams params = mSessionParams.get(session);
if (params == null) return null;
return params.postMessageHandler.getOriginForTesting();
}
/** /**
* See {@link PostMessageHandler#reset(WebContents)}. * See {@link PostMessageHandler#reset(WebContents)}.
*/ */
...@@ -543,7 +565,7 @@ class ClientManager { ...@@ -543,7 +565,7 @@ class ClientManager {
if (params == null) return; if (params == null) return;
mSessionParams.remove(session); mSessionParams.remove(session);
if (params.postMessageHandler != null) { if (params.postMessageHandler != null) {
params.postMessageHandler.unbindFromContext(mContext); params.postMessageHandler.cleanup(mContext);
} }
if (params.disconnectCallback != null) params.disconnectCallback.run(session); if (params.disconnectCallback != null) params.disconnectCallback.run(session);
mUidHasCalledWarmup.delete(params.uid); mUidHasCalledWarmup.delete(params.uid);
......
...@@ -503,8 +503,17 @@ public class CustomTabsConnection { ...@@ -503,8 +503,17 @@ public class CustomTabsConnection {
// If the API is not enabled, we don't set the post message origin, which will // If the API is not enabled, we don't set the post message origin, which will
// avoid PostMessageHandler initialization and disallow postMessage calls. // avoid PostMessageHandler initialization and disallow postMessage calls.
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_POST_MESSAGE_API)) return; if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_POST_MESSAGE_API)) return;
mClientManager.initializeWithPostMessageOriginForSession(
session, verifyOriginForSession(session, uid, postMessageOrigin)); // Attempt to verify origin synchronously. If successful directly initialize
// postMessage channel for session.
Uri verifiedOrigin = verifyOriginForSession(session, uid, postMessageOrigin);
if (verifiedOrigin == null) {
mClientManager.verifyAndInitializeWithPostMessageOriginForSession(
session, postMessageOrigin);
} else {
mClientManager.initializeWithPostMessageOriginForSession(
session, verifiedOrigin);
}
} }
}); });
return true; return true;
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.content.browser.BrowserStartupController;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
/**
* Used to verify postMessage origin for a designated package name.
*
* Uses Digital Asset Links to confirm that the given origin is associated with the package name as
* a postMessage origin. It caches any origin that has been verified during the current application
* lifecycle and reuses that without making any new network requests.
*
* The lifecycle of this object is governed by the owner. The owner has to call
* {@link OriginVerifier#cleanUp()} for proper cleanup of dependencies.
*/
@JNINamespace("customtabs")
class OriginVerifier {
private static final String TAG = "OriginVerifier";
private static final char[] HEX_CHAR_LOOKUP = "0123456789ABCDEF".toCharArray();
private static Map<String, Uri> sCachedOriginMap;
private final OriginVerificationListener mListener;
private final String mPackageName;
private final String mSignatureFingerprint;
private long mNativeOriginVerifier = 0;
private Uri mOrigin;
/**
* To be used for prepopulating verified origin for testing functionality.
* @param packageName The package name to prepopulate for.
* @param origin The origin to add as verified.
*/
@VisibleForTesting
static void prePopulateVerifiedOriginForTesting(String packageName, Uri origin) {
cacheVerifiedOriginIfNeeded(packageName, origin);
}
private static Uri getPostMessageOriginFromVerifiedOrigin(
String packageName, Uri verifiedOrigin) {
return Uri.parse(IntentHandler.ANDROID_APP_REFERRER_SCHEME + "://"
+ verifiedOrigin.getHost() + "/" + packageName);
}
private static void cacheVerifiedOriginIfNeeded(String packageName, Uri origin) {
if (sCachedOriginMap == null) sCachedOriginMap = new HashMap<>();
if (!sCachedOriginMap.containsKey(packageName)) {
sCachedOriginMap.put(packageName, origin);
}
}
/**
* Callback interface for getting verification results.
*/
public interface OriginVerificationListener {
/**
* To be posted on the handler thread after the verification finishes.
* @param packageName The package name for the origin verification query for this result.
* @param origin The origin that was declared on the query for this result.
* @param verified Whether the given origin was verified to correspond to the given package.
*/
void onOriginVerified(String packageName, Uri origin, boolean verified);
}
/**
* Main constructor.
* Use {@link OriginVerifier#start(Uri)}
* @param listener The listener who will get the verification result.
* @param packageName The package for the Android application for verification.
*/
public OriginVerifier(OriginVerificationListener listener, String packageName) {
mListener = listener;
mPackageName = packageName;
mSignatureFingerprint = getCertificateSHA256FingerprintForPackage(mPackageName);
}
/**
* Verify the claimed origin for the cached package name asynchronously. This will end up
* making a network request for non-cached origins with a URLFetcher using the last used
* profile as context.
* @param origin The postMessage origin the application is claiming to have. Can't be null.
*/
public void start(@NonNull Uri origin) {
ThreadUtils.assertOnUiThread();
mOrigin = origin;
// If this origin is cached as verified already, use that.
Uri cachedOrigin = getCachedOriginIfExists();
if (cachedOrigin != null && cachedOrigin.equals(origin)) {
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
originVerified(true);
}
});
return;
}
if (mNativeOriginVerifier != 0) cleanUp();
if (!BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.isStartupSuccessfullyCompleted()) {
// Early return for testing without native.
return;
}
mNativeOriginVerifier = nativeInit(Profile.getLastUsedProfile().getOriginalProfile());
assert mNativeOriginVerifier != 0;
boolean success = nativeVerifyOrigin(
mNativeOriginVerifier, mPackageName, mSignatureFingerprint, mOrigin.toString());
if (!success) {
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
originVerified(false);
}
});
}
}
/**
* Cleanup native dependencies on this object.
*/
void cleanUp() {
if (mNativeOriginVerifier == 0) return;
nativeDestroy(mNativeOriginVerifier);
mNativeOriginVerifier = 0;
}
private static PackageInfo getPackageInfo(String packageName) {
PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
// Will return null if there is no package found.
}
return packageInfo;
}
/**
* Computes the SHA256 certificate for the given package name. The app with the given package
* name has to be installed on device. The output will be a 30 long HEX string with : between
* each value.
* @param packageName The package name to query the signature for.
* @return The SHA256 certificate for the package name.
*/
static String getCertificateSHA256FingerprintForPackage(String packageName) {
PackageInfo packageInfo = getPackageInfo(packageName);
if (packageInfo == null) return null;
InputStream input = new ByteArrayInputStream(packageInfo.signatures[0].toByteArray());
X509Certificate certificate = null;
String hexString = null;
try {
certificate =
(X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(
input);
hexString = byteArrayToHexString(
MessageDigest.getInstance("SHA256").digest(certificate.getEncoded()));
} catch (CertificateEncodingException e) {
Log.w(TAG, "Certificate type X509 encoding failed");
} catch (CertificateException | NoSuchAlgorithmException e) {
// This shouldn't happen.
}
return hexString;
}
/**
* Converts a byte array to hex string with : inserted between each element.
* @param byteArray The array to be converted.
* @return A string with two letters representing each byte and : in between.
*/
static String byteArrayToHexString(byte[] byteArray) {
StringBuilder hexString = new StringBuilder(byteArray.length * 3 - 1);
for (int i = 0; i < byteArray.length; ++i) {
hexString.append(HEX_CHAR_LOOKUP[(byteArray[i] & 0xf0) >>> 4]);
hexString.append(HEX_CHAR_LOOKUP[byteArray[i] & 0xf]);
if (i < (byteArray.length - 1)) hexString.append(':');
}
return hexString.toString();
}
@CalledByNative
private void originVerified(boolean originVerified) {
if (originVerified) {
cacheVerifiedOriginIfNeeded(mPackageName, mOrigin);
mOrigin = getPostMessageOriginFromVerifiedOrigin(mPackageName, mOrigin);
}
mListener.onOriginVerified(mPackageName, mOrigin, originVerified);
cleanUp();
}
private Uri getCachedOriginIfExists() {
if (sCachedOriginMap == null) return null;
return sCachedOriginMap.get(mPackageName);
}
private native long nativeInit(Profile profile);
private native boolean nativeVerifyOrigin(long nativeOriginVerifier, String packageName,
String signatureFingerprint, String origin);
private native void nativeDestroy(long nativeOriginVerifier);
}
...@@ -6,12 +6,15 @@ package org.chromium.chrome.browser.customtabs; ...@@ -6,12 +6,15 @@ package org.chromium.chrome.browser.customtabs;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsService; import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsSessionToken; import android.support.customtabs.CustomTabsSessionToken;
import android.support.customtabs.PostMessageServiceConnection; import android.support.customtabs.PostMessageServiceConnection;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.customtabs.OriginVerifier.OriginVerificationListener;
import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content.browser.AppWebMessagePort; import org.chromium.content.browser.AppWebMessagePort;
import org.chromium.content_public.browser.MessagePort; import org.chromium.content_public.browser.MessagePort;
...@@ -22,13 +25,16 @@ import org.chromium.content_public.browser.WebContentsObserver; ...@@ -22,13 +25,16 @@ import org.chromium.content_public.browser.WebContentsObserver;
/** /**
* A class that handles postMessage communications with a designated {@link CustomTabsSessionToken}. * A class that handles postMessage communications with a designated {@link CustomTabsSessionToken}.
*/ */
public class PostMessageHandler extends PostMessageServiceConnection { public class PostMessageHandler
extends PostMessageServiceConnection implements OriginVerificationListener {
private final MessageCallback mMessageCallback; private final MessageCallback mMessageCallback;
private OriginVerifier mOriginVerifier;
private WebContents mWebContents; private WebContents mWebContents;
private boolean mMessageChannelCreated; private boolean mMessageChannelCreated;
private boolean mBoundToService; private boolean mBoundToService;
private AppWebMessagePort[] mChannel; private AppWebMessagePort[] mChannel;
private Uri mOrigin; private Uri mOrigin;
private String mPackageName;
/** /**
* Basic constructor. Everytime the given {@link CustomTabsSessionToken} is associated with a * Basic constructor. Everytime the given {@link CustomTabsSessionToken} is associated with a
...@@ -48,6 +54,14 @@ public class PostMessageHandler extends PostMessageServiceConnection { ...@@ -48,6 +54,14 @@ public class PostMessageHandler extends PostMessageServiceConnection {
}; };
} }
/**
* Sets the package name unique to the session.
* @param packageName The package name for the client app for the owning session.
*/
void setPackageName(@NonNull String packageName) {
mPackageName = packageName;
}
/** /**
* Resets the internal state of the handler, linking the associated * Resets the internal state of the handler, linking the associated
* {@link CustomTabsSessionToken} with a new {@link WebContents} and the {@link Tab} that * {@link CustomTabsSessionToken} with a new {@link WebContents} and the {@link Tab} that
...@@ -126,6 +140,22 @@ public class PostMessageHandler extends PostMessageServiceConnection { ...@@ -126,6 +140,22 @@ public class PostMessageHandler extends PostMessageServiceConnection {
} }
} }
/**
* Asynchronously verify the postMessage origin for the given package name and initialize with
* it if the result is a success. Can be called multiple times. If so, the previous requests
* will be overridden.
* @param origin The origin to verify for.
*/
public void verifyAndInitializeWithOrigin(final Uri origin) {
if (mOriginVerifier == null) mOriginVerifier = new OriginVerifier(this, mPackageName);
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
mOriginVerifier.start(origin);
}
});
}
/** /**
* Relay a postMessage request through the current channel assigned to this session. * Relay a postMessage request through the current channel assigned to this session.
* @param message The message to be sent. * @param message The message to be sent.
...@@ -166,4 +196,27 @@ public class PostMessageHandler extends PostMessageServiceConnection { ...@@ -166,4 +196,27 @@ public class PostMessageHandler extends PostMessageServiceConnection {
public void onPostMessageServiceDisconnected() { public void onPostMessageServiceDisconnected() {
mBoundToService = false; mBoundToService = false;
} }
@Override
public void onOriginVerified(String packageName, Uri origin, boolean result) {
if (!result) return;
initializeWithOrigin(origin);
}
/**
* @return The origin that has been declared for this handler.
*/
@VisibleForTesting
Uri getOriginForTesting() {
return mOrigin;
}
/**
* Cleans up any dependencies that this handler might have.
* @param context Context to use for unbinding if necessary.
*/
void cleanup(Context context) {
if (mBoundToService) super.unbindFromContext(context);
if (mOriginVerifier != null) mOriginVerifier.cleanUp();
}
} }
...@@ -279,6 +279,7 @@ chrome_java_sources = [ ...@@ -279,6 +279,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java", "java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java",
"java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionService.java", "java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionService.java",
"java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java", "java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java",
"java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java",
"java/src/org/chromium/chrome/browser/customtabs/PostMessageHandler.java", "java/src/org/chromium/chrome/browser/customtabs/PostMessageHandler.java",
"java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java", "java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity.java", "java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity.java",
...@@ -1354,6 +1355,7 @@ chrome_test_java_sources = [ ...@@ -1354,6 +1355,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/customtabs/CustomTabsTestUtils.java", "javatests/src/org/chromium/chrome/browser/customtabs/CustomTabsTestUtils.java",
"javatests/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistenceIntegrationTest.java", "javatests/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistenceIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicyTest.java", "javatests/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicyTest.java",
"javatests/src/org/chromium/chrome/browser/customtabs/OriginVerifierTest.java",
"javatests/src/org/chromium/chrome/browser/customtabs/RequestThrottlerTest.java", "javatests/src/org/chromium/chrome/browser/customtabs/RequestThrottlerTest.java",
"javatests/src/org/chromium/chrome/browser/document/LauncherActivityTest.java", "javatests/src/org/chromium/chrome/browser/document/LauncherActivityTest.java",
"javatests/src/org/chromium/chrome/browser/dom_distiller/DistillabilityServiceTest.java", "javatests/src/org/chromium/chrome/browser/dom_distiller/DistillabilityServiceTest.java",
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package org.chromium.chrome.browser.customtabs; package org.chromium.chrome.browser.customtabs;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Process; import android.os.Process;
import android.support.customtabs.CustomTabsSessionToken; import android.support.customtabs.CustomTabsSessionToken;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
...@@ -16,11 +17,15 @@ import org.junit.Rule; ...@@ -16,11 +17,15 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.BaseJUnit4ClassRunner; import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.MetricsUtils; import org.chromium.base.test.util.MetricsUtils;
import org.chromium.base.test.util.RetryOnFailure; import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.content.browser.test.NativeLibraryTestRule; import org.chromium.content.browser.test.NativeLibraryTestRule;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
/** Tests for ClientManager. */ /** Tests for ClientManager. */
@RunWith(BaseJUnit4ClassRunner.class) @RunWith(BaseJUnit4ClassRunner.class)
...@@ -152,6 +157,36 @@ public class ClientManagerTest { ...@@ -152,6 +157,36 @@ public class ClientManagerTest {
mClientManager.getPredictionOutcome(mSession, URL + "#fragment")); mClientManager.getPredictionOutcome(mSession, URL + "#fragment"));
} }
@Test
@SmallTest
public void testPostMessageOriginVerification() {
Assert.assertTrue(
mClientManager.newSession(mSession, mUid, null, new PostMessageHandler(mSession)));
// Should always start with no origin.
Assert.assertNull(mClientManager.getPostMessageOriginForSessionForTesting(mSession));
// With no prepopulated origins, this verification should fail.
mClientManager.verifyAndInitializeWithPostMessageOriginForSession(mSession, Uri.parse(URL));
Assert.assertNull(mClientManager.getPostMessageOriginForSessionForTesting(mSession));
// If there is a prepopulated origin, we should get a synchronous verification.
OriginVerifier.prePopulateVerifiedOriginForTesting(
ContextUtils.getApplicationContext().getPackageName(), Uri.parse(URL));
mClientManager.verifyAndInitializeWithPostMessageOriginForSession(mSession, Uri.parse(URL));
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mClientManager.getPostMessageOriginForSessionForTesting(mSession) != null;
}
});
Uri verifiedOrigin = mClientManager.getPostMessageOriginForSessionForTesting(mSession);
Assert.assertEquals(IntentHandler.ANDROID_APP_REFERRER_SCHEME, verifiedOrigin.getScheme());
// initializeWithPostMessageOriginForSession should override without checking origin.
mClientManager.initializeWithPostMessageOriginForSession(mSession, null);
Assert.assertNull(mClientManager.getPostMessageOriginForSessionForTesting(mSession));
}
@Test @Test
@SmallTest @SmallTest
public void testFirstLowConfidencePredictionIsNotThrottled() { public void testFirstLowConfidencePredictionIsNotThrottled() {
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
/** Tests for OriginVerifier. */
@RunWith(BaseJUnit4ClassRunner.class)
public class OriginVerifierTest {
private static final byte[] BYTE_ARRAY = new byte[] {(byte) 0xaa, (byte) 0xbb, (byte) 0xcc,
(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x01, (byte) 0x02};
private static final String STRING_ARRAY = "AA:BB:CC:10:20:30:01:02";
private static final String PACKAGE_NAME = "org.chromium.chrome";
private static final String SHA_256_FINGERPRINT =
"32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B"
+ ":22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0";
@Test
@SmallTest
public void testSHA256CertificateChecks() {
Assert.assertEquals(OriginVerifier.byteArrayToHexString(BYTE_ARRAY), STRING_ARRAY);
Assert.assertEquals(OriginVerifier.getCertificateSHA256FingerprintForPackage(PACKAGE_NAME),
SHA_256_FINGERPRINT);
}
}
...@@ -2728,6 +2728,8 @@ split_static_library("browser") { ...@@ -2728,6 +2728,8 @@ split_static_library("browser") {
"android/contextualsearch/resolved_search_term.h", "android/contextualsearch/resolved_search_term.h",
"android/cookies/cookies_fetcher.cc", "android/cookies/cookies_fetcher.cc",
"android/cookies/cookies_fetcher.h", "android/cookies/cookies_fetcher.h",
"android/customtabs/origin_verifier.cc",
"android/customtabs/origin_verifier.h",
"android/data_usage/data_use_matcher.cc", "android/data_usage/data_use_matcher.cc",
"android/data_usage/data_use_matcher.h", "android/data_usage/data_use_matcher.h",
"android/data_usage/data_use_tab_helper.cc", "android/data_usage/data_use_tab_helper.cc",
...@@ -2752,6 +2754,8 @@ split_static_library("browser") { ...@@ -2752,6 +2754,8 @@ split_static_library("browser") {
"android/devtools_manager_delegate_android.h", "android/devtools_manager_delegate_android.h",
"android/devtools_server.cc", "android/devtools_server.cc",
"android/devtools_server.h", "android/devtools_server.h",
"android/digital_asset_links/digital_asset_links_handler.cc",
"android/digital_asset_links/digital_asset_links_handler.h",
"android/document/document_web_contents_delegate.cc", "android/document/document_web_contents_delegate.cc",
"android/document/document_web_contents_delegate.h", "android/document/document_web_contents_delegate.h",
"android/dom_distiller/distiller_ui_handle_android.cc", "android/dom_distiller/distiller_ui_handle_android.cc",
...@@ -4097,6 +4101,7 @@ if (is_android) { ...@@ -4097,6 +4101,7 @@ if (is_android) {
"../android/java/src/org/chromium/chrome/browser/contextualsearch/CtrSuppression.java", "../android/java/src/org/chromium/chrome/browser/contextualsearch/CtrSuppression.java",
"../android/java/src/org/chromium/chrome/browser/cookies/CookiesFetcher.java", "../android/java/src/org/chromium/chrome/browser/cookies/CookiesFetcher.java",
"../android/java/src/org/chromium/chrome/browser/crash/MinidumpUploadService.java", "../android/java/src/org/chromium/chrome/browser/crash/MinidumpUploadService.java",
"../android/java/src/org/chromium/chrome/browser/customtabs/OriginVerifier.java",
"../android/java/src/org/chromium/chrome/browser/customtabs/ResourcePrefetchPredictor.java", "../android/java/src/org/chromium/chrome/browser/customtabs/ResourcePrefetchPredictor.java",
"../android/java/src/org/chromium/chrome/browser/database/SQLiteCursor.java", "../android/java/src/org/chromium/chrome/browser/database/SQLiteCursor.java",
"../android/java/src/org/chromium/chrome/browser/datausage/DataUseTabUIManager.java", "../android/java/src/org/chromium/chrome/browser/datausage/DataUseTabUIManager.java",
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
#include "chrome/browser/android/contextualsearch/contextual_search_tab_helper.h" #include "chrome/browser/android/contextualsearch/contextual_search_tab_helper.h"
#include "chrome/browser/android/contextualsearch/ctr_suppression.h" #include "chrome/browser/android/contextualsearch/ctr_suppression.h"
#include "chrome/browser/android/cookies/cookies_fetcher.h" #include "chrome/browser/android/cookies/cookies_fetcher.h"
#include "chrome/browser/android/customtabs/origin_verifier.h"
#include "chrome/browser/android/data_usage/data_use_tab_ui_manager_android.h" #include "chrome/browser/android/data_usage/data_use_tab_ui_manager_android.h"
#include "chrome/browser/android/data_usage/external_data_use_observer_bridge.h" #include "chrome/browser/android/data_usage/external_data_use_observer_bridge.h"
#include "chrome/browser/android/devtools_server.h" #include "chrome/browser/android/devtools_server.h"
...@@ -358,6 +359,7 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = { ...@@ -358,6 +359,7 @@ static base::android::RegistrationMethod kChromeRegisteredMethods[] = {
{"OmniboxUrlEmphasizer", {"OmniboxUrlEmphasizer",
OmniboxUrlEmphasizer::RegisterOmniboxUrlEmphasizer}, OmniboxUrlEmphasizer::RegisterOmniboxUrlEmphasizer},
{"OmniboxViewUtil", OmniboxViewUtil::RegisterOmniboxViewUtil}, {"OmniboxViewUtil", OmniboxViewUtil::RegisterOmniboxViewUtil},
{"OriginVerifier", customtabs::RegisterOriginVerifier},
{"OverlayPanelContent", RegisterOverlayPanelContent}, {"OverlayPanelContent", RegisterOverlayPanelContent},
{"PartnerBookmarksReader", {"PartnerBookmarksReader",
PartnerBookmarksReader::RegisterPartnerBookmarksReader}, PartnerBookmarksReader::RegisterPartnerBookmarksReader},
......
yusufo@chromium.org
lizeb@chromium.org
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/customtabs/origin_verifier.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/values.h"
#include "chrome/browser/android/digital_asset_links/digital_asset_links_handler.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_android.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "content/public/browser/browser_thread.h"
#include "jni/OriginVerifier_jni.h"
using base::android::ConvertJavaStringToUTF16;
using base::android::JavaParamRef;
namespace customtabs {
OriginVerifier::OriginVerifier(JNIEnv* env, jobject obj, jobject jprofile) {
jobject_.Reset(env, obj);
Profile* profile = ProfileAndroid::FromProfileAndroid(jprofile);
DCHECK(profile);
asset_link_handler_ =
base::MakeUnique<digital_asset_links::DigitalAssetLinksHandler>(
profile->GetRequestContext());
}
OriginVerifier::~OriginVerifier() = default;
bool OriginVerifier::VerifyOrigin(JNIEnv* env,
const JavaParamRef<jobject>& obj,
const JavaParamRef<jstring>& j_package_name,
const JavaParamRef<jstring>& j_fingerprint,
const JavaParamRef<jstring>& j_origin) {
if (!j_package_name || !j_fingerprint || !j_origin)
return false;
std::string package_name = ConvertJavaStringToUTF8(env, j_package_name);
std::string fingerprint = ConvertJavaStringToUTF8(env, j_fingerprint);
std::string origin = ConvertJavaStringToUTF8(env, j_origin);
// Multiple calls here will end up resetting the callback on the handler side
// and cancelling previous requests.
// If during the request this verifier gets killed, the handler and the
// UrlFetcher making the request will also get killed, so we won't get any
// dangling callback reference issues.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return asset_link_handler_->CheckDigitalAssetLinkRelationship(
base::Bind(&customtabs::OriginVerifier::OnRelationshipCheckComplete,
base::Unretained(this)),
origin, package_name, fingerprint,
"delegate_permission/common.use_as_origin");
}
void OriginVerifier::OnRelationshipCheckComplete(
std::unique_ptr<base::DictionaryValue> response) {
JNIEnv* env = base::android::AttachCurrentThread();
bool verified = false;
if (response) {
response->GetBoolean(
digital_asset_links::kDigitalAssetLinksCheckResponseKeyLinked,
&verified);
}
Java_OriginVerifier_originVerified(env, jobject_, verified);
}
void OriginVerifier::Destroy(JNIEnv* env,
const base::android::JavaRef<jobject>& obj) {
delete this;
}
static jlong Init(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<jobject>& jprofile) {
if (!g_browser_process)
return 0;
OriginVerifier* native_verifier = new OriginVerifier(env, obj, jprofile);
return reinterpret_cast<intptr_t>(native_verifier);
}
bool RegisterOriginVerifier(JNIEnv* env) {
return RegisterNativesImpl(env);
}
} // namespace customtabs
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_ANDROID_CUSTOMTABS_ORIGIN_VERIFIER_H_
#define CHROME_BROWSER_ANDROID_CUSTOMTABS_ORIGIN_VERIFIER_H_
#include "base/android/scoped_java_ref.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"
namespace base {
class DictionaryValue;
}
namespace digital_asset_links {
class DigitalAssetLinksHandler;
}
namespace customtabs {
// JNI bridge for OriginVerifier.java
class OriginVerifier {
public:
OriginVerifier(JNIEnv* env, jobject obj, jobject jprofile);
~OriginVerifier();
// Verify origin with the given parameters. No network requests can be made
// if the params are null.
bool VerifyOrigin(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<jstring>& j_package_name,
const base::android::JavaParamRef<jstring>& j_fingerprint,
const base::android::JavaParamRef<jstring>& j_origin);
void Destroy(JNIEnv* env, const base::android::JavaRef<jobject>& obj);
private:
void OnRelationshipCheckComplete(
std::unique_ptr<base::DictionaryValue> response);
std::unique_ptr<digital_asset_links::DigitalAssetLinksHandler>
asset_link_handler_;
base::android::ScopedJavaGlobalRef<jobject> jobject_;
DISALLOW_COPY_AND_ASSIGN(OriginVerifier);
};
bool RegisterOriginVerifier(JNIEnv* env);
} // namespace customtabs
#endif // CHROME_BROWSER_ANDROID_CUSTOMTABS_ORIGIN_VERIFIER_H_
# It is likely that this code will eventually be shared across platforms, so
# excluding dependencies that would make this being a component impossible.
include_rules = [
"-content",
"-chrome",
"+base",
"+content/public/test",
"+chrome/browser/android/digital_asset_links",
"+net",
]
yusufo@chromium.org
lizeb@chromium.org
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/digital_asset_links/digital_asset_links_handler.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "components/safe_json/safe_json_parser.h"
#include "net/base/load_flags.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_request_status.h"
namespace {
const char kDigitalAssetLinksBaseURL[] =
"https://digitalassetlinks.googleapis.com";
const char kDigitalAssetLinksCheckAPI[] = "/v1/assetlinks:check?";
const char kTargetOriginParam[] = "source.web.site";
const char kSourcePackageNameParam[] = "target.androidApp.packageName";
const char kSourceFingerprintParam[] =
"target.androidApp.certificate.sha256Fingerprint";
const char kRelationshipParam[] = "relation";
GURL GetUrlForCheckingRelationship(const std::string& web_domain,
const std::string& package_name,
const std::string& fingerprint,
const std::string& relationship) {
GURL request_url =
GURL(kDigitalAssetLinksBaseURL).Resolve(kDigitalAssetLinksCheckAPI);
request_url =
net::AppendQueryParameter(request_url, kTargetOriginParam, web_domain);
request_url = net::AppendQueryParameter(request_url, kSourcePackageNameParam,
package_name);
request_url = net::AppendQueryParameter(request_url, kSourceFingerprintParam,
fingerprint);
request_url =
net::AppendQueryParameter(request_url, kRelationshipParam, relationship);
DCHECK(request_url.is_valid());
return request_url;
}
} // namespace
namespace digital_asset_links {
const char kDigitalAssetLinksCheckResponseKeyLinked[] = "linked";
DigitalAssetLinksHandler::DigitalAssetLinksHandler(
const scoped_refptr<net::URLRequestContextGetter>& request_context)
: request_context_(request_context), weak_ptr_factory_(this) {}
DigitalAssetLinksHandler::~DigitalAssetLinksHandler() = default;
void DigitalAssetLinksHandler::OnURLFetchComplete(
const net::URLFetcher* source) {
if (!source->GetStatus().is_success() ||
source->GetResponseCode() != net::HTTP_OK) {
LOG(WARNING) << base::StringPrintf(
"Digital Asset Links endpoint responded with code %d.",
source->GetResponseCode());
callback_.Run(nullptr);
return;
}
std::string response_body;
source->GetResponseAsString(&response_body);
safe_json::SafeJsonParser::Parse(
response_body,
base::Bind(&DigitalAssetLinksHandler::OnJSONParseSucceeded,
weak_ptr_factory_.GetWeakPtr()),
base::Bind(&DigitalAssetLinksHandler::OnJSONParseFailed,
weak_ptr_factory_.GetWeakPtr()));
url_fetcher_.reset(nullptr);
}
void DigitalAssetLinksHandler::OnJSONParseSucceeded(
std::unique_ptr<base::Value> result) {
callback_.Run(base::DictionaryValue::From(std::move(result)));
}
void DigitalAssetLinksHandler::OnJSONParseFailed(
const std::string& error_message) {
LOG(WARNING)
<< base::StringPrintf(
"Digital Asset Links response parsing failed with message:")
<< error_message;
callback_.Run(nullptr);
}
bool DigitalAssetLinksHandler::CheckDigitalAssetLinkRelationship(
RelationshipCheckResultCallback callback,
const std::string& web_domain,
const std::string& package_name,
const std::string& fingerprint,
const std::string& relationship) {
GURL request_url = GetUrlForCheckingRelationship(web_domain, package_name,
fingerprint, relationship);
if (!request_url.is_valid())
return false;
// Resetting both the callback and URLFetcher here to ensure that any previous
// requests will never get a OnUrlFetchComplete. This effectively cancels
// any checks that was done over this handler.
callback_ = callback;
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("digital_asset_links", R"(
semantics {
sender: "Digital Asset Links Handler"
description:
"Digital Asset Links APIs allows any caller to check pre declared"
"relationships between two assets which can be either web domains"
"or native applications. This requests checks for a specific "
"relationship declared by a web site with an Android application"
trigger:
"When the related application makes a claim to have the queried"
"relationship with the web domain"
destination: WEBSITE
}
policy {
cookies_allowed: true
cookies_store: "user"
setting: "Not user controlled. But the verification is a trusted API"
"that doesn't use user data"
policy_exception_justification:
"Not implemented, considered not useful as no content is being "
"uploaded; this request merely downloads the resources on the web."
})");
url_fetcher_ = net::URLFetcher::Create(0, request_url, net::URLFetcher::GET,
this, traffic_annotation);
url_fetcher_->SetAutomaticallyRetryOn5xx(false);
url_fetcher_->SetRequestContext(request_context_.get());
url_fetcher_->Start();
return true;
}
} // namespace digital_asset_links
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_ANDROID_DIGITAL_ASSET_LINKS_DIGITAL_ASSET_LINKS_HANDLER_H_
#define CHROME_BROWSER_ANDROID_DIGITAL_ASSET_LINKS_DIGITAL_ASSET_LINKS_HANDLER_H_
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"
namespace base {
class DictionaryValue;
}
namespace digital_asset_links {
extern const char kDigitalAssetLinksCheckResponseKeyLinked[];
typedef base::Callback<void(std::unique_ptr<base::DictionaryValue>)>
RelationshipCheckResultCallback;
// A handler class for sending REST API requests to DigitalAssetLinks web
// end point. See
// https://developers.google.com/digital-asset-links/v1/getting-started
// for details of usage and APIs. These APIs are used to verify declared
// relationships between different asset types like web domains or Android apps.
// The lifecycle of this handler will be governed by the owner.
class DigitalAssetLinksHandler : public net::URLFetcherDelegate {
public:
explicit DigitalAssetLinksHandler(
const scoped_refptr<net::URLRequestContextGetter>& request_context);
~DigitalAssetLinksHandler() override;
// Checks whether the given "relationship" has been declared by the target
// |web_domain| for the source Android app which is uniquely defined by the
// |package_name| and SHA256 |fingerprint| (a string with 32 hexadecimals
// with : in between) given. Any error in the string params
// here will result in a bad request and a nullptr response to the callback.
//
// Calling this multiple times on the same handler will cancel the previous
// checks.
// See
// https://developers.google.com/digital-asset-links/reference/rest/v1/assetlinks/check
// for details.
bool CheckDigitalAssetLinkRelationship(
RelationshipCheckResultCallback callback,
const std::string& web_domain,
const std::string& package_name,
const std::string& fingerprint,
const std::string& relationship);
private:
// net::URLFetcherDelegate:
void OnURLFetchComplete(const net::URLFetcher* source) override;
// Callbacks for the SafeJsonParser.
void OnJSONParseSucceeded(std::unique_ptr<base::Value> result);
void OnJSONParseFailed(const std::string& error_message);
scoped_refptr<net::URLRequestContextGetter> request_context_;
std::unique_ptr<net::URLFetcher> url_fetcher_;
// The per request callback for receiving a URLFetcher result. This gets
// reset every time we get a new CheckDigitalAssetLinkRelationship call.
RelationshipCheckResultCallback callback_;
base::WeakPtrFactory<DigitalAssetLinksHandler> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(DigitalAssetLinksHandler);
};
} // namespace digital_asset_links
#endif // CHROME_BROWSER_ANDROID_DIGITAL_ASSET_LINKS_DIGITAL_ASSET_LINKS_HANDLER_H_
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/digital_asset_links/digital_asset_links_handler.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/values.h"
#include "components/safe_json/testing_json_parser.h"
#include "content/public/test/test_browser_thread.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_status.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace digital_asset_links {
namespace {
class DigitalAssetLinksHandlerTest : public ::testing::Test {
public:
DigitalAssetLinksHandlerTest()
: num_invocations_(0),
response_(nullptr),
io_thread_(content::BrowserThread::IO, &message_loop_) {}
void OnRelationshipCheckComplete(
std::unique_ptr<base::DictionaryValue> response) {
++num_invocations_;
response_ = std::move(response);
}
protected:
void SetUp() override { num_invocations_ = 0; }
void SendResponse(net::Error error, int response_code, bool linked) {
net::TestURLFetcher* fetcher = url_fetcher_factory_.GetFetcherByID(0);
ASSERT_TRUE(fetcher);
fetcher->set_status(net::URLRequestStatus::FromError(error));
fetcher->set_response_code(response_code);
if (error == net::OK && response_code == net::HTTP_OK && linked) {
fetcher->SetResponseString(
R"({
"linked": true ,
"maxAge": "40.188652381s"
})");
} else if (error == net::OK && response_code == net::HTTP_OK) {
fetcher->SetResponseString(
R"({
"linked": false ,
"maxAge": "40.188652381s"
})");
} else if (error == net::OK && response_code == net::HTTP_BAD_REQUEST) {
fetcher->SetResponseString(
R"({
"code": 400 ,
"message": "Invalid statement query received."
"status": "INVALID_ARGUMENT"
})");
} else {
fetcher->SetResponseString("");
}
fetcher->delegate()->OnURLFetchComplete(fetcher);
base::RunLoop().RunUntilIdle();
}
int num_invocations_;
std::unique_ptr<base::DictionaryValue> response_;
private:
base::MessageLoop message_loop_;
safe_json::TestingJsonParser::ScopedFactoryOverride factory_override_;
content::TestBrowserThread io_thread_;
net::TestURLFetcherFactory url_fetcher_factory_;
DISALLOW_COPY_AND_ASSIGN(DigitalAssetLinksHandlerTest);
};
} // namespace
TEST_F(DigitalAssetLinksHandlerTest, PositiveResponse) {
DigitalAssetLinksHandler handler(nullptr);
handler.CheckDigitalAssetLinkRelationship(
base::Bind(&DigitalAssetLinksHandlerTest::OnRelationshipCheckComplete,
base::Unretained(this)),
"", "", "", "");
SendResponse(net::OK, net::HTTP_OK, true);
bool verified = false;
EXPECT_EQ(1, num_invocations_);
EXPECT_TRUE(response_);
response_->GetBoolean(
digital_asset_links::kDigitalAssetLinksCheckResponseKeyLinked, &verified);
EXPECT_TRUE(verified);
}
TEST_F(DigitalAssetLinksHandlerTest, NegativeResponse) {
DigitalAssetLinksHandler handler(nullptr);
handler.CheckDigitalAssetLinkRelationship(
base::Bind(&DigitalAssetLinksHandlerTest::OnRelationshipCheckComplete,
base::Unretained(this)),
"", "", "", "");
SendResponse(net::OK, net::HTTP_OK, false);
bool verified = false;
EXPECT_EQ(1, num_invocations_);
EXPECT_TRUE(response_);
response_->GetBoolean(
digital_asset_links::kDigitalAssetLinksCheckResponseKeyLinked, &verified);
EXPECT_FALSE(verified);
}
TEST_F(DigitalAssetLinksHandlerTest, BadRequest) {
DigitalAssetLinksHandler handler(nullptr);
handler.CheckDigitalAssetLinkRelationship(
base::Bind(&DigitalAssetLinksHandlerTest::OnRelationshipCheckComplete,
base::Unretained(this)),
"", "", "", "");
SendResponse(net::OK, net::HTTP_BAD_REQUEST, true);
EXPECT_EQ(1, num_invocations_);
EXPECT_FALSE(response_);
}
TEST_F(DigitalAssetLinksHandlerTest, NetworkError) {
DigitalAssetLinksHandler handler(nullptr);
handler.CheckDigitalAssetLinkRelationship(
base::Bind(&DigitalAssetLinksHandlerTest::OnRelationshipCheckComplete,
base::Unretained(this)),
"", "", "", "");
SendResponse(net::ERR_ABORTED, net::HTTP_OK, true);
EXPECT_EQ(1, num_invocations_);
EXPECT_FALSE(response_);
}
} // namespace digital_asset_links
...@@ -2870,6 +2870,7 @@ test("unit_tests") { ...@@ -2870,6 +2870,7 @@ test("unit_tests") {
"../browser/android/data_usage/external_data_use_observer_unittest.cc", "../browser/android/data_usage/external_data_use_observer_unittest.cc",
"../browser/android/data_usage/external_data_use_reporter_unittest.cc", "../browser/android/data_usage/external_data_use_reporter_unittest.cc",
"../browser/android/data_usage/tab_data_use_entry_unittest.cc", "../browser/android/data_usage/tab_data_use_entry_unittest.cc",
"../browser/android/digital_asset_links/digital_asset_links_handler_unittest.cc",
"../browser/android/download/download_manager_service_unittest.cc", "../browser/android/download/download_manager_service_unittest.cc",
"../browser/android/history_report/data_observer_unittest.cc", "../browser/android/history_report/data_observer_unittest.cc",
"../browser/android/history_report/delta_file_backend_leveldb_unittest.cc", "../browser/android/history_report/delta_file_backend_leveldb_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment