diff --git a/sdk/build.gradle b/sdk/build.gradle index f78c5ddc..92e7a319 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -7,6 +7,11 @@ plugins { id "com.github.spotbugs" version "5.2.3" id "org.owasp.dependencycheck" version "7.1.1" id "org.sonarqube" version "3.4.0.2513" + id "de.undercouch.download" version "5.5.0" +} + +ext { + hcaptchaLoaderVersion = "1.2.4" } android { @@ -18,7 +23,7 @@ android { } defaultConfig { - minSdkVersion 16 + minSdkVersion 16 // Android 4.1 targetSdkVersion 34 // See https://developer.android.com/studio/publish/versioning @@ -31,6 +36,7 @@ android { versionName "4.0.0" buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\"" + buildConfigField 'String', 'LOADER_VERSION', "\"${hcaptchaLoaderVersion}\"" consumerProguardFiles "consumer-rules.pro" proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -111,7 +117,31 @@ project.afterEvaluate { } } -long MAX_AAR_SIZE_KB = 200 +long MAX_AAR_SIZE_KB = 250 + +String sha256Integrity(String filename) { + def file = new File("${buildDir}/generated/assets/hcaptcha/${filename}") + def sha256Hash = file.withInputStream { stream -> + org.apache.commons.codec.digest.DigestUtils.sha256(stream) + } + def base64Hash = org.apache.commons.codec.binary.Base64.encodeBase64String(sha256Hash) + + return "sha256-${base64Hash}" +} + +tasks.register('downloadPolyfillsJs', Download) { + src "https://www.unpkg.com/@hcaptcha/loader@${hcaptchaLoaderVersion}/dist/polyfills.js" + dest layout.buildDirectory.file("generated/assets/hcaptcha/polyfills.js") + onlyIfModified true +} + +tasks.register('downloadHCaptchaLoaderJs', Download) { + src "https://www.unpkg.com/@hcaptcha/loader@${hcaptchaLoaderVersion}/dist/index.es5.js" + dest layout.buildDirectory.file("generated/assets/hcaptcha/loader.js") + onlyIfModified true +}.get().dependsOn('downloadPolyfillsJs') + +android.sourceSets.main.assets.srcDirs += [layout.buildDirectory.file("generated/assets")] android.libraryVariants.all { variant -> def variantName = variant.name.capitalize() @@ -136,7 +166,7 @@ android.libraryVariants.all { variant -> var aarFile = variant.packageLibraryProvider.get().archiveFile.get().getAsFile() long aarSizeKb = aarFile.length() / 1024 if (aarSizeKb > MAX_AAR_SIZE_KB) { - throw new GradleException("${aarPath} size exceeded! ${aarSizeKb}Kbyte > ${MAX_AAR_SIZE_KB}Kbyte") + throw new GradleException("${aarFile} size exceeded! ${aarSizeKb}Kbyte > ${MAX_AAR_SIZE_KB}Kbyte") } } }) @@ -150,9 +180,14 @@ android.libraryVariants.all { variant -> doFirst { def outputJavaClass = file("$outputDir/HCaptchaHtml.java") def template = file("$projectDir/src/main/html/HCaptchaHtml.java.tml").text + def polyfillsIntegrity = sha256Integrity('polyfills.js') + def loaderIntegrity = sha256Integrity('loader.js') def html = file("$projectDir/src/main/html/hcaptcha.html") .readLines() .stream() + .map({l -> "${l.replaceAll('@LOADER_VERSION@', hcaptchaLoaderVersion)}"}) + .map({l -> "${l.replaceAll('@POLYFILLS_INTEGRITY@', polyfillsIntegrity)}"}) + .map({l -> "${l.replaceAll('@LOADER_INTEGRITY@', loaderIntegrity)}"}) .map({l -> "\"${l.replaceAll('"', '\\\\"')}\\n\""}) .collect(java.util.stream.Collectors.joining("\n${' ' * 16}+ ")) @@ -167,8 +202,15 @@ android.libraryVariants.all { variant -> } } - // preBuild.dependsOn generateTask variant.registerJavaGeneratingTask(generateTask, outputDir) + generateTask.dependsOn(downloadHCaptchaLoaderJs) + + tasks.named("package${variant.name.capitalize()}Assets") + .get() + .dependsOn(downloadHCaptchaLoaderJs) + tasks.named("merge${variant.name.capitalize()}Assets") + .get() + .dependsOn(downloadHCaptchaLoaderJs) } apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle" \ No newline at end of file diff --git a/sdk/src/main/html/hcaptcha.html b/sdk/src/main/html/hcaptcha.html index 770989c4..051908e4 100644 --- a/sdk/src/main/html/hcaptcha.html +++ b/sdk/src/main/html/hcaptcha.html @@ -36,7 +36,9 @@ window.sysDebug = JSON.parse(window.JSDI.getSysDebug()); } - + + diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java index c964ccf7..a59e0331 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java @@ -194,6 +194,7 @@ public void onAnimationEnd(Animator animation) { @Override public void onLoaded() { + HCaptchaLog.d("DialogFragment.onLoaded"); assert webViewHelper != null; if (webViewHelper.getConfig().getSize() != HCaptchaSize.INVISIBLE) { diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java index 60cd0a60..91f445f3 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaHeadlessWebView.java @@ -70,6 +70,7 @@ public void onSuccess(final String token) { @Override public void onLoaded() { + HCaptchaLog.d("HeadlessWebView.onLoaded"); webViewLoaded = true; if (shouldResetOnLoad) { shouldResetOnLoad = false; diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java index e86b58c3..5cd5a387 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java @@ -22,6 +22,12 @@ import lombok.Getter; import lombok.NonNull; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + final class HCaptchaWebViewHelper { @NonNull private final Context context; @@ -120,12 +126,17 @@ public boolean shouldRetry(HCaptchaException exception) { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private class HCaptchaWebClient extends WebViewClient { + private final Map assetsCache = new HashMap<>(); @NonNull private final Handler handler; HCaptchaWebClient(@NonNull Handler handler) { this.handler = handler; + + final String baseUrl = "https://unpkg.com/@hcaptcha/loader@" + BuildConfig.LOADER_VERSION + "/dist"; + assetsCache.put(Uri.parse(baseUrl + "/index.es5.js"), "hcaptcha/loader.js"); + assetsCache.put(Uri.parse(baseUrl + "/polyfills.js"), "hcaptcha/polyfills.js"); } private String stripUrl(String url) { @@ -135,7 +146,23 @@ private String stripUrl(String url) { @Override public WebResourceResponse shouldInterceptRequest (final WebView view, final WebResourceRequest request) { final Uri requestUri = request.getUrl(); - if (requestUri != null && requestUri.getScheme() != null && requestUri.getScheme().equals("http")) { + final String assetPath = assetsCache.get(requestUri); + if (assetPath != null) { + try { + HCaptchaLog.d("[webview] shouldInterceptRequest return local asset for " + requestUri); + return new WebResourceResponse( + "application/javascript", + "UTF-8", + 200, + "OK", + Collections.singletonMap("Access-Control-Allow-Origin", + Objects.toString(config.getHost(), "null")), + view.getContext().getAssets().open(assetPath) + ); + } catch (IOException e) { + HCaptchaLog.w("[webview] shouldInterceptRequest wasn't able to load " + assetPath + " from assets"); + } + } else if (requestUri != null && requestUri.getScheme() != null && requestUri.getScheme().equals("http")) { handler.post(() -> { webView.removeJavascriptInterface(HCaptchaJSInterface.JS_INTERFACE_TAG); webView.removeJavascriptInterface(HCaptchaDebugInfo.JS_INTERFACE_TAG); diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java index 4501c9e3..259ff4bc 100644 --- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java +++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaWebViewHelperTest.java @@ -2,15 +2,20 @@ import static com.hcaptcha.sdk.AssertUtil.failAsNonReachable; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import android.content.Context; +import android.content.res.AssetManager; import android.app.Activity; import android.os.Handler; import android.os.Looper; import androidx.test.core.app.ActivityScenario; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.platform.app.InstrumentationRegistry; import com.hcaptcha.sdk.test.TestActivity; @@ -18,6 +23,8 @@ import org.junit.Rule; import org.junit.Test; +import java.io.IOException; +import java.io.InputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -88,4 +95,22 @@ public void onFailure(HCaptchaException e) { assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); } + + @Test + public void testLoaderJsAssetPresence() throws IOException { + AssetManager assets = InstrumentationRegistry + .getInstrumentation() + .getTargetContext() + .getAssets(); + assertNotNull(assets.open("hcaptcha/loader.js")); + } + + @Test + public void testPolyfillsJsAssetPresence() throws IOException { + AssetManager assets = InstrumentationRegistry + .getInstrumentation() + .getTargetContext() + .getAssets(); + assertNotNull(assets.open("hcaptcha/polyfills.js")); + } }