Skip to content

Commit

Permalink
feat: migrate to hcaptcha-loader
Browse files Browse the repository at this point in the history
  • Loading branch information
CAMOBAP committed Jun 26, 2024
1 parent efd18b2 commit 315e7c9
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 88 deletions.
22 changes: 21 additions & 1 deletion sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.3"
}

android {
Expand All @@ -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'
Expand Down Expand Up @@ -113,6 +119,14 @@ project.afterEvaluate {

long MAX_AAR_SIZE_KB = 200

tasks.register('downloadHCaptchaLoaderJs', Download) {
src "https://www.unpkg.com/@hcaptcha/loader@${hcaptchaLoaderVersion}/dist/index.mjs"
dest layout.buildDirectory.file("generated/assets/hcaptcha/loader.mjs")
onlyIfModified true
}

android.sourceSets.main.assets.srcDirs += [layout.buildDirectory.file("generated/assets")]

android.libraryVariants.all { variant ->
def variantName = variant.name.capitalize()
project.task("report${variantName}AarSize") {
Expand Down Expand Up @@ -153,6 +167,7 @@ android.libraryVariants.all { variant ->
def html = file("$projectDir/src/main/html/hcaptcha.html")
.readLines()
.stream()
.map({l -> "${l.replaceAll('@LOADER_VERSION@', hcaptchaLoaderVersion)}"})
.map({l -> "\"${l.replaceAll('"', '\\\\"')}\\n\""})
.collect(java.util.stream.Collectors.joining("\n${' ' * 16}+ "))

Expand All @@ -167,8 +182,13 @@ android.libraryVariants.all { variant ->
}
}

// preBuild.dependsOn generateTask
variant.registerJavaGeneratingTask(generateTask, outputDir)
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"
172 changes: 86 additions & 86 deletions sdk/src/main/html/hcaptcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
window.sysDebug = JSON.parse(window.JSDI.getSysDebug());
}
</script>
<script type="text/javascript">
<script type="module">
import { hCaptchaLoader } from 'https://www.unpkg.com/@hcaptcha/loader@@LOADER_VERSION@/dist/index.mjs';

// Android will inject this bridge object as `JSInterface`
// Browser is missing it, so we mock it
var BridgeObject = window.JSInterface || {
Expand Down Expand Up @@ -70,20 +72,21 @@
}
};
var bridgeConfig = JSON.parse(BridgeObject.getConfig());
var hCaptchaID = null;
/**
* Called programmatically from HCaptchaWebViewHelper.
*/
function resetAndExecute() {
async function resetAndExecute() {
hcaptcha.reset();
hcaptcha.execute(hCaptchaID);
await execute();
}
window.resetAndExecute = resetAndExecute;
function reset() {
hcaptcha.reset();
}
function getTheme(bridgeConfig) {
var theme = bridgeConfig.theme;
var customTheme = bridgeConfig.customTheme;

function getTheme(config) {
var theme = config.theme;
var customTheme = config.customTheme;
if (customTheme) {
try {
return JSON.parse(customTheme);
Expand All @@ -94,112 +97,109 @@
}
return theme;
}
function getRenderConfig() {
function getRenderConfig(config) {
return {
sitekey: bridgeConfig.siteKey,
size: bridgeConfig.size,
orientation: bridgeConfig.orientation,
theme: getTheme(bridgeConfig),
sitekey: config.siteKey,
size: config.size,
orientation: config.orientation,
theme: getTheme(config),
host: config.host || config.siteKey + '.android-sdk.hcaptcha.com',
callback: function callback(token) {
console.log("callback");
return BridgeObject.onPass(token);
},
'chalexpired-callback': function chalexpiredCallback() {
console.log("chalexpired-callback");
return BridgeObject.onError(15);
},
'close-callback': function closeCallback() {
console.log("close-callback");
return BridgeObject.onError(30);
},
'error-callback': function errorCallback(error) {
switch(error) {
case "rate-limited":
return BridgeObject.onError(31);
case "network-error":
return BridgeObject.onError(7);
case "invalid-data":
return BridgeObject.onError(8);
case "challenge-error":
return BridgeObject.onError(9);
case "internal-error":
return BridgeObject.onError(10);
default:
// Error not handled? Log it for debugging purposes
console.error(error);
return BridgeObject.onError(29);
}
},
'open-callback': function openCallback() {
return BridgeObject.onOpen();
}
};
}
function onHcaptchaLoaded() {
function getScriptParams(config) {
return {
scriptLocation: document.head,
apihost: config.jsSrc,
loadAsync: true,
async: true,
};
};
function getLoaderParams(config) {
var result = getScriptParams(config);

result.render = 'explicit';
result.sentry = config.sentry;
result.custom = !!config.customTheme
result.assethost = config.assethost;
result.imghost = config.imghost;
result.reportapi = config.reportapi;
result.endpoint = config.endpoint;
result.host = config.host || config.siteKey + '.android-sdk.hcaptcha.com';
result.recaptchacompat = 'off';
result.hl = config.locale;
result.cleanup = true;

return result;
};
var container = document.getElementById("hcaptcha-container");
container.addEventListener("click", function () {
if (hcaptcha) {
// Allows dismissal of checkbox view
hcaptcha.close();
} else {
BridgeObject.onError(30);
}
});
async function execute() {
try {
var renderConfig = getRenderConfig();
hCaptchaID = hcaptcha.render('hcaptcha-container', renderConfig);
const { response } = await hcaptcha.execute(getScriptParams(bridgeConfig)); // { async: true }
BridgeObject.onPass(response);
} catch (error) {
switch(error) {
case "rate-limited":
return BridgeObject.onError(31);
case "network-error":
return BridgeObject.onError(7);
case "invalid-data":
return BridgeObject.onError(8);
case "challenge-error":
return BridgeObject.onError(9);
case "internal-error":
return BridgeObject.onError(10);
default:
// Error not handled? Log it for debugging purposes
console.error(error);
return BridgeObject.onError(29);
}
}
}
async function loadApi(config) {
try {
window.hcaptcha = await hCaptchaLoader(getLoaderParams(config));
var renderConfig = getRenderConfig(config);
hcaptcha.render("hcaptcha-container", renderConfig);
BridgeObject.onLoaded();
var rqdata = bridgeConfig.rqdata;
var rqdata = config.rqdata;
if (rqdata) {
hcaptcha.setData(hCaptchaID, { rqdata: rqdata });
hcaptcha.setData({ rqdata: rqdata });
}
if (renderConfig.size === 'invisible' && !bridgeConfig.hideDialog) {
if (renderConfig.size === 'invisible' && !config.hideDialog) {
// We want to auto execute in case of `invisible` checkbox.
// But not in case of `hideDialog` since verification process
// might be desired to happen at a later time.
hcaptcha.execute(hCaptchaID);
await execute();
}
} catch (e) {
console.error(e);
console.error("loadApi error", e);
BridgeObject.onError(29);
}
}
function addQueryParamIfDefined(url, queryName, queryValue) {
if (queryValue !== undefined && queryValue !== null) {
var link = url.indexOf('?') !== -1 ? '&' : '?';
return url + link + queryName + '=' + encodeURIComponent(queryValue);
}
return url;
}
function loadApi() {
var siteKey = bridgeConfig.siteKey;
var locale = bridgeConfig.locale;
var sentry = bridgeConfig.sentry;
var jsSrc = bridgeConfig.jsSrc;
var endpoint = bridgeConfig.endpoint;
var assethost = bridgeConfig.assethost;
var imghost = bridgeConfig.imghost;
var reportapi = bridgeConfig.reportapi;
var host = bridgeConfig.host || siteKey + '.android-sdk.hcaptcha.com';
var scriptSrc = jsSrc + '?render=explicit&onload=' + onHcaptchaLoaded.name;
scriptSrc = addQueryParamIfDefined(scriptSrc, 'recaptchacompat', 'off');
scriptSrc = addQueryParamIfDefined(scriptSrc, 'hl', locale);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'host', host);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'sentry', sentry);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'endpoint', endpoint);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'assethost', assethost);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'imghost', imghost);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'reportapi', reportapi);
if (bridgeConfig.customTheme) {
scriptSrc = addQueryParamIfDefined(scriptSrc, 'custom', 'true');
}
var script = document.createElement('script');
script.async = true;
script.src = scriptSrc;
script.onerror = function () {
// network issue
BridgeObject.onError(7);
};
document.head.appendChild(script);
}
var container = document.getElementById("hcaptcha-container");
container.addEventListener("click", function () {
if (window.hcaptcha) {
// Allows dismissal of checkbox view
window.hcaptcha.close();
} else {
BridgeObject.onError(30);
}
});
loadApi();
loadApi(bridgeConfig);
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public void onSuccess(final String token) {

@Override
public void onLoaded() {
HCaptchaLog.d("HeadlessWebView.onLoaded");
webViewLoaded = true;
if (shouldResetOnLoad) {
shouldResetOnLoad = false;
Expand Down
23 changes: 22 additions & 1 deletion sdk/src/main/java/com/hcaptcha/sdk/HCaptchaWebViewHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
import lombok.Getter;
import lombok.NonNull;

import java.io.IOException;
import java.util.Collections;
import java.util.Objects;

final class HCaptchaWebViewHelper {
@NonNull
private final Context context;
Expand Down Expand Up @@ -120,6 +124,9 @@ public boolean shouldRetry(HCaptchaException exception) {

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private class HCaptchaWebClient extends WebViewClient {
private final Uri loaderUri = Uri.parse(
"https://www.unpkg.com/@hcaptcha/loader@" + BuildConfig.LOADER_VERSION + "/dist/index.mjs"
);

@NonNull
private final Handler handler;
Expand All @@ -135,7 +142,21 @@ 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")) {
if (loaderUri.equals(requestUri)) {
try {
return new WebResourceResponse(
"application/javascript",
"UTF-8",
200,
"OK",
Collections.singletonMap("Access-Control-Allow-Origin",
Objects.toString(config.getHost(), "null")),
view.getContext().getAssets().open("hcaptcha/loader.mjs")
);
} catch (IOException e) {
HCaptchaLog.w("WebViewHelper wasn't able to load loader.mjs 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@

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.app.Activity;
import android.os.Handler;
import android.os.Looper;

import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.platform.app.InstrumentationRegistry;

import com.hcaptcha.sdk.test.TestActivity;

import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -88,4 +92,10 @@ public void onFailure(HCaptchaException e) {

assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
}

@Test
public void testLoaderJsAssetPresence() throws IOException {
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertNotNull(appContext.getAssets().open("hcaptcha/loader.mjs"));
}
}

0 comments on commit 315e7c9

Please sign in to comment.