catalyst-core 0.1.0-canary.4 → 0.1.0-canary.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native/androidProject/app/build.gradle.kts +20 -0
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +4 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +21 -18
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +19 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +104 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +0 -71
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +12 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +4 -3
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
- package/dist/native/bridge/constants/NativeInterfaces.js +5 -3
- package/dist/native/bridge/hooks.js +28 -2
- package/dist/native/bridge/useBaseHook.js +3 -5
- package/dist/native/bridge/utils/NativeBridge.js +11 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -6
- package/dist/native/iosnativeWebView/Package.resolved +0 -126
- package/dist/native/iosnativeWebView/Package.swift +8 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +9 -2
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +41 -6
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +173 -187
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +83 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +7 -1
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +76 -7
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +1 -127
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
- package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +46 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +381 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +452 -0
- package/dist/scripts/loadEnvironmentVariables.js +3 -1
- package/package.json +1 -1
|
@@ -15,6 +15,18 @@ val keystorePassword: String? by project.properties // Changed from keyStorePas
|
|
|
15
15
|
val keyAlias: String? by project.properties
|
|
16
16
|
val keyPassword: String? by project.properties
|
|
17
17
|
|
|
18
|
+
fun isAllowBackupEnabled(): Boolean {
|
|
19
|
+
return try {
|
|
20
|
+
val propsFile = File("${project.projectDir}/src/main/assets/webview_config.properties")
|
|
21
|
+
if (!propsFile.exists()) return false
|
|
22
|
+
val props = Properties()
|
|
23
|
+
props.load(propsFile.inputStream())
|
|
24
|
+
props.getProperty("android.security.allowBackup", "false").trim().lowercase() == "true"
|
|
25
|
+
} catch (e: Exception) {
|
|
26
|
+
false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
fun isNotificationsEnabled(): Boolean {
|
|
19
31
|
return try {
|
|
20
32
|
if (configPath == null) return false
|
|
@@ -56,6 +68,7 @@ android {
|
|
|
56
68
|
versionName = "1.1"
|
|
57
69
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
58
70
|
buildConfigField("String", "LOCAL_IP", "\"${getLocalIpAddress()}\"")
|
|
71
|
+
manifestPlaceholders["allowBackup"] = isAllowBackupEnabled()
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
// Add signing configuration for app bundle
|
|
@@ -148,6 +161,13 @@ android {
|
|
|
148
161
|
abortOnError = true
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
// Configure test options for unit tests
|
|
165
|
+
testOptions {
|
|
166
|
+
unitTests {
|
|
167
|
+
isReturnDefaultValues = true
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
151
171
|
// Conditional source sets based on notifications config
|
|
152
172
|
sourceSets {
|
|
153
173
|
getByName("main") {
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -15,8 +15,7 @@ import androidx.webkit.WebResourceRequestCompat
|
|
|
15
15
|
import android.widget.ProgressBar
|
|
16
16
|
import androidx.webkit.WebViewAssetLoader
|
|
17
17
|
import io.yourname.androidproject.WebCacheManager
|
|
18
|
-
import io.yourname.androidproject.
|
|
19
|
-
import io.yourname.androidproject.isExternalDomain
|
|
18
|
+
import io.yourname.androidproject.URLWhitelistManager
|
|
20
19
|
import io.yourname.androidproject.matchesCachePattern
|
|
21
20
|
import io.yourname.androidproject.utils.CameraUtils
|
|
22
21
|
import io.yourname.androidproject.utils.NetworkUtils
|
|
@@ -100,6 +99,9 @@ class CustomWebView(
|
|
|
100
99
|
.getProperty("accessControl.enabled", "true")
|
|
101
100
|
.equals("true", ignoreCase = true)
|
|
102
101
|
|
|
102
|
+
// Initialize URLWhitelistManager with access control configuration
|
|
103
|
+
URLWhitelistManager.initialize(accessControlEnabled, allowedUrls)
|
|
104
|
+
|
|
103
105
|
// Set initial flags based on buildOptimisation
|
|
104
106
|
if (buildOptimisation) {
|
|
105
107
|
isInitialApiCalled = false
|
|
@@ -187,6 +189,10 @@ class CustomWebView(
|
|
|
187
189
|
webView.clearHistory()
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
fun clearAllCache() {
|
|
193
|
+
cacheManager.clearAll()
|
|
194
|
+
}
|
|
195
|
+
|
|
190
196
|
fun canGoBack(): Boolean = webView.canGoBack()
|
|
191
197
|
|
|
192
198
|
fun goBack() {
|
|
@@ -480,7 +486,7 @@ class CustomWebView(
|
|
|
480
486
|
}
|
|
481
487
|
|
|
482
488
|
// Check if URL is allowed
|
|
483
|
-
if (
|
|
489
|
+
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
484
490
|
if (BuildConfig.DEBUG) {
|
|
485
491
|
Log.w(TAG, "🙫 SERVICE_WORKER: Request blocked by access control: $url")
|
|
486
492
|
}
|
|
@@ -624,7 +630,7 @@ class CustomWebView(
|
|
|
624
630
|
}
|
|
625
631
|
|
|
626
632
|
// Check if URL is an external domain
|
|
627
|
-
if (url.scheme in listOf("http", "https") && isExternalDomain(urlString
|
|
633
|
+
if (url.scheme in listOf("http", "https") && URLWhitelistManager.isExternalDomain(urlString)) {
|
|
628
634
|
if (BuildConfig.DEBUG) {
|
|
629
635
|
Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
|
|
630
636
|
}
|
|
@@ -633,7 +639,7 @@ class CustomWebView(
|
|
|
633
639
|
}
|
|
634
640
|
|
|
635
641
|
// Check if URL is allowed for internal navigation
|
|
636
|
-
if (
|
|
642
|
+
if (!URLWhitelistManager.isUrlAllowed(urlString)) {
|
|
637
643
|
if (BuildConfig.DEBUG) {
|
|
638
644
|
Log.w(TAG, "🚫 URL blocked by access control: $urlString")
|
|
639
645
|
}
|
|
@@ -666,21 +672,18 @@ class CustomWebView(
|
|
|
666
672
|
Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
|
|
667
673
|
}
|
|
668
674
|
|
|
669
|
-
if (
|
|
670
|
-
if (
|
|
671
|
-
|
|
672
|
-
Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
|
|
673
|
-
Log.w(TAG, "🚫 STEP_1_FAIL: Allowed URLs: $allowedUrls")
|
|
674
|
-
}
|
|
675
|
-
// Return an empty response to block the request
|
|
676
|
-
return WebResourceResponse("text/plain", "utf-8", null)
|
|
677
|
-
} else {
|
|
678
|
-
if (BuildConfig.DEBUG) {
|
|
679
|
-
Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
|
|
680
|
-
}
|
|
675
|
+
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
676
|
+
if (BuildConfig.DEBUG) {
|
|
677
|
+
Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
|
|
681
678
|
}
|
|
679
|
+
// Return an empty response to block the request
|
|
680
|
+
return WebResourceResponse("text/plain", "utf-8", null)
|
|
682
681
|
} else if (BuildConfig.DEBUG) {
|
|
683
|
-
|
|
682
|
+
if (URLWhitelistManager.isAccessControlEnabled()) {
|
|
683
|
+
Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
|
|
684
|
+
} else {
|
|
685
|
+
Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
|
|
686
|
+
}
|
|
684
687
|
}
|
|
685
688
|
|
|
686
689
|
// TODO: Add HTML file workflow - index.html not available yet
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -141,6 +141,25 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
141
141
|
// Public method for NativeBridge to get current safe area insets
|
|
142
142
|
fun getCurrentSafeAreaInsets(): SafeAreaInsets = latestSafeAreaInsets
|
|
143
143
|
|
|
144
|
+
fun clearWebCache() {
|
|
145
|
+
customWebView.clearAllCache()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun setScreenSecure(enable: Boolean) {
|
|
149
|
+
if (enable) {
|
|
150
|
+
window.setFlags(
|
|
151
|
+
WindowManager.LayoutParams.FLAG_SECURE,
|
|
152
|
+
WindowManager.LayoutParams.FLAG_SECURE
|
|
153
|
+
)
|
|
154
|
+
} else {
|
|
155
|
+
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fun isScreenSecure(): Boolean {
|
|
160
|
+
return (window.attributes.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
/**
|
|
145
164
|
* Notify WebView when safe area insets are updated
|
|
146
165
|
* Called when deferred inset calculation completes
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt
CHANGED
|
@@ -1029,6 +1029,110 @@ class NativeBridge(
|
|
|
1029
1029
|
}
|
|
1030
1030
|
}
|
|
1031
1031
|
|
|
1032
|
+
@JavascriptInterface
|
|
1033
|
+
fun setScreenSecure(params: String?) {
|
|
1034
|
+
mainActivity.runOnUiThread {
|
|
1035
|
+
try {
|
|
1036
|
+
val enable = try {
|
|
1037
|
+
org.json.JSONObject(params ?: "{}").optBoolean("enable", true)
|
|
1038
|
+
} catch (e: Exception) {
|
|
1039
|
+
true
|
|
1040
|
+
}
|
|
1041
|
+
mainActivity.setScreenSecure(enable)
|
|
1042
|
+
BridgeUtils.notifyWeb(
|
|
1043
|
+
webView,
|
|
1044
|
+
BridgeUtils.WebEvents.ON_SCREEN_SECURE_SET,
|
|
1045
|
+
"""{"secure": $enable, "success": true}"""
|
|
1046
|
+
)
|
|
1047
|
+
} catch (e: Exception) {
|
|
1048
|
+
// Use notifyWebJson to prevent JSON injection from e.message
|
|
1049
|
+
BridgeUtils.notifyWebJson(
|
|
1050
|
+
webView,
|
|
1051
|
+
BridgeUtils.WebEvents.ON_SCREEN_SECURE_ERROR,
|
|
1052
|
+
org.json.JSONObject().apply {
|
|
1053
|
+
put("success", false)
|
|
1054
|
+
put("error", e.message ?: "setScreenSecure failed")
|
|
1055
|
+
}
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
@JavascriptInterface
|
|
1062
|
+
fun getScreenSecure(params: String?) {
|
|
1063
|
+
mainActivity.runOnUiThread {
|
|
1064
|
+
try {
|
|
1065
|
+
val isSecure = mainActivity.isScreenSecure()
|
|
1066
|
+
BridgeUtils.notifyWebJson(
|
|
1067
|
+
webView,
|
|
1068
|
+
BridgeUtils.WebEvents.ON_SCREEN_SECURE_STATUS,
|
|
1069
|
+
org.json.JSONObject().apply {
|
|
1070
|
+
put("secure", isSecure)
|
|
1071
|
+
put("success", true)
|
|
1072
|
+
}
|
|
1073
|
+
)
|
|
1074
|
+
} catch (e: Exception) {
|
|
1075
|
+
BridgeUtils.notifyWebJson(
|
|
1076
|
+
webView,
|
|
1077
|
+
BridgeUtils.WebEvents.ON_SCREEN_SECURE_ERROR,
|
|
1078
|
+
org.json.JSONObject().apply {
|
|
1079
|
+
put("success", false)
|
|
1080
|
+
put("error", e.message ?: "getScreenSecure failed")
|
|
1081
|
+
}
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
@JavascriptInterface
|
|
1088
|
+
fun clearWebData(params: String?) {
|
|
1089
|
+
mainActivity.runOnUiThread {
|
|
1090
|
+
try {
|
|
1091
|
+
webView.clearCache(true)
|
|
1092
|
+
webView.clearHistory()
|
|
1093
|
+
android.webkit.WebStorage.getInstance().deleteAllData()
|
|
1094
|
+
mainActivity.clearWebCache()
|
|
1095
|
+
// CookieManager.removeAllCookies is async — fire success only from its callback
|
|
1096
|
+
android.webkit.CookieManager.getInstance().removeAllCookies { cookiesRemoved ->
|
|
1097
|
+
android.webkit.CookieManager.getInstance().flush()
|
|
1098
|
+
mainActivity.runOnUiThread {
|
|
1099
|
+
try {
|
|
1100
|
+
// Use notifyWebJson to prevent JSON injection; cookiesRemoved is a
|
|
1101
|
+
// Boolean so string interpolation is safe, but keep consistent pattern
|
|
1102
|
+
BridgeUtils.notifyWebJson(
|
|
1103
|
+
webView,
|
|
1104
|
+
BridgeUtils.WebEvents.ON_WEB_DATA_CLEARED,
|
|
1105
|
+
org.json.JSONObject().apply {
|
|
1106
|
+
put("success", true)
|
|
1107
|
+
put("cookiesRemoved", cookiesRemoved)
|
|
1108
|
+
}
|
|
1109
|
+
)
|
|
1110
|
+
} catch (e: Exception) {
|
|
1111
|
+
BridgeUtils.notifyWebJson(
|
|
1112
|
+
webView,
|
|
1113
|
+
BridgeUtils.WebEvents.ON_WEB_DATA_CLEAR_ERROR,
|
|
1114
|
+
org.json.JSONObject().apply {
|
|
1115
|
+
put("success", false)
|
|
1116
|
+
put("error", e.message ?: "clearWebData cookie flush failed")
|
|
1117
|
+
}
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} catch (e: Exception) {
|
|
1123
|
+
// Use notifyWebJson to prevent JSON injection from e.message
|
|
1124
|
+
BridgeUtils.notifyWebJson(
|
|
1125
|
+
webView,
|
|
1126
|
+
BridgeUtils.WebEvents.ON_WEB_DATA_CLEAR_ERROR,
|
|
1127
|
+
org.json.JSONObject().apply {
|
|
1128
|
+
put("success", false)
|
|
1129
|
+
put("error", e.message ?: "clearWebData failed")
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1032
1136
|
@JavascriptInterface
|
|
1033
1137
|
fun getSafeArea(data: String? = null) {
|
|
1034
1138
|
BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_SAFE_AREA_INSETS_UPDATED, "get safe area") {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
package io.yourname.androidproject
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manager for URL whitelisting and access control
|
|
7
|
+
* Provides centralized URL validation with pattern matching support
|
|
8
|
+
*
|
|
9
|
+
* Pattern matching rules:
|
|
10
|
+
* - *text* or text (no wildcard) -> checks if URL contains "text"
|
|
11
|
+
* - text* -> checks if URL starts with "text"
|
|
12
|
+
* - *text -> checks if URL ends with "text"
|
|
13
|
+
*
|
|
14
|
+
* URL processing:
|
|
15
|
+
* - Query parameters and hash fragments are stripped before matching
|
|
16
|
+
* - Matching is case-insensitive
|
|
17
|
+
* - Ports are kept as-is (user must include port in pattern if URL has explicit port)
|
|
18
|
+
*/
|
|
19
|
+
object URLWhitelistManager {
|
|
20
|
+
|
|
21
|
+
private const val TAG = "URLWhitelistManager"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Data class to hold categorized URL patterns for efficient matching
|
|
25
|
+
*/
|
|
26
|
+
private data class CategorizedPatterns(
|
|
27
|
+
val contains: List<String>,
|
|
28
|
+
val prefix: List<String>,
|
|
29
|
+
val suffix: List<String>
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@Volatile
|
|
33
|
+
private var accessControlEnabled: Boolean = false
|
|
34
|
+
|
|
35
|
+
@Volatile
|
|
36
|
+
private var patterns: CategorizedPatterns? = null
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize the whitelist manager with configuration
|
|
40
|
+
* Should be called once during app initialization
|
|
41
|
+
*/
|
|
42
|
+
@Synchronized
|
|
43
|
+
fun initialize(enabled: Boolean, allowedUrls: List<String>) {
|
|
44
|
+
this.accessControlEnabled = enabled
|
|
45
|
+
this.patterns = categorizePatterns(allowedUrls)
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (BuildConfig.DEBUG) {
|
|
49
|
+
Log.d(TAG, "URLWhitelistManager initialized")
|
|
50
|
+
Log.d(TAG, "Access Control Enabled: $enabled")
|
|
51
|
+
Log.d(TAG, "Allowed URLs: $allowedUrls")
|
|
52
|
+
patterns?.let {
|
|
53
|
+
Log.d(TAG, "Categorized - Contains: ${it.contains}")
|
|
54
|
+
Log.d(TAG, "Categorized - Prefix: ${it.prefix}")
|
|
55
|
+
Log.d(TAG, "Categorized - Suffix: ${it.suffix}")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e: RuntimeException) {
|
|
59
|
+
// Log not available in unit tests - silently ignore
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if access control is enabled
|
|
65
|
+
*/
|
|
66
|
+
fun isAccessControlEnabled(): Boolean {
|
|
67
|
+
return accessControlEnabled
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if a URL is allowed according to the whitelist patterns
|
|
72
|
+
* If access control is disabled, all URLs are allowed
|
|
73
|
+
*/
|
|
74
|
+
fun isUrlAllowed(url: String): Boolean {
|
|
75
|
+
// If access control is disabled, allow everything
|
|
76
|
+
if (!accessControlEnabled) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Always allow framework server URLs (localhost with /framework- pattern)
|
|
81
|
+
// These are internal infrastructure URLs used for large file handling
|
|
82
|
+
val frameworkUrlPattern = Regex("^https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?/framework-[a-zA-Z0-9_-]+", RegexOption.IGNORE_CASE)
|
|
83
|
+
if (frameworkUrlPattern.containsMatchIn(url)) {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val patterns = this.patterns ?: return false
|
|
88
|
+
|
|
89
|
+
// If all pattern lists are empty, block everything
|
|
90
|
+
if (patterns.contains.isEmpty() && patterns.prefix.isEmpty() && patterns.suffix.isEmpty()) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
val cleanUrl = cleanUrl(url)
|
|
95
|
+
|
|
96
|
+
// Check contains patterns
|
|
97
|
+
if (patterns.contains.any { cleanUrl.contains(it) }) {
|
|
98
|
+
try {
|
|
99
|
+
if (BuildConfig.DEBUG) {
|
|
100
|
+
Log.d(TAG, "✅ URL allowed (contains match): $url")
|
|
101
|
+
}
|
|
102
|
+
} catch (e: RuntimeException) { /* Ignore in tests */ }
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check prefix patterns
|
|
107
|
+
if (patterns.prefix.any { cleanUrl.startsWith(it) }) {
|
|
108
|
+
try {
|
|
109
|
+
if (BuildConfig.DEBUG) {
|
|
110
|
+
Log.d(TAG, "✅ URL allowed (prefix match): $url")
|
|
111
|
+
}
|
|
112
|
+
} catch (e: RuntimeException) { /* Ignore in tests */ }
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check suffix patterns
|
|
117
|
+
if (patterns.suffix.any { cleanUrl.endsWith(it) }) {
|
|
118
|
+
try {
|
|
119
|
+
if (BuildConfig.DEBUG) {
|
|
120
|
+
Log.d(TAG, "✅ URL allowed (suffix match): $url")
|
|
121
|
+
}
|
|
122
|
+
} catch (e: RuntimeException) { /* Ignore in tests */ }
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (BuildConfig.DEBUG) {
|
|
128
|
+
Log.w(TAG, "🚫 URL blocked by access control: $url")
|
|
129
|
+
Log.w(TAG, "🚫 Clean URL: $cleanUrl")
|
|
130
|
+
}
|
|
131
|
+
} catch (e: RuntimeException) { /* Ignore in tests */ }
|
|
132
|
+
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if a URL is from an external domain (not in whitelist)
|
|
138
|
+
* If access control is disabled, nothing is considered external
|
|
139
|
+
*/
|
|
140
|
+
fun isExternalDomain(url: String): Boolean {
|
|
141
|
+
// If access control is disabled, nothing is external
|
|
142
|
+
if (!accessControlEnabled) {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Simply the inverse of isUrlAllowed
|
|
147
|
+
return !isUrlAllowed(url)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Clean URL by removing query parameters and hash, then lowercase for case-insensitive matching
|
|
152
|
+
* Also decodes URL-encoded characters to prevent bypass attempts
|
|
153
|
+
*/
|
|
154
|
+
private fun cleanUrl(url: String): String {
|
|
155
|
+
// Decode URL-encoded characters first to prevent bypass (e.g., %3F for ?, %23 for #)
|
|
156
|
+
val decoded = try {
|
|
157
|
+
java.net.URLDecoder.decode(url, "UTF-8")
|
|
158
|
+
} catch (e: Exception) {
|
|
159
|
+
// If decode fails, use original URL
|
|
160
|
+
url
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var cleanUrl = decoded
|
|
164
|
+
|
|
165
|
+
// Remove hash fragment (# and everything after)
|
|
166
|
+
val hashIndex = cleanUrl.indexOf('#')
|
|
167
|
+
if (hashIndex != -1) {
|
|
168
|
+
cleanUrl = cleanUrl.substring(0, hashIndex)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove query parameters (? and everything after)
|
|
172
|
+
val queryIndex = cleanUrl.indexOf('?')
|
|
173
|
+
if (queryIndex != -1) {
|
|
174
|
+
cleanUrl = cleanUrl.substring(0, queryIndex)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return cleanUrl.lowercase()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Categorize patterns into contains, prefix, and suffix for efficient matching
|
|
182
|
+
*/
|
|
183
|
+
private fun categorizePatterns(allowedUrls: List<String>): CategorizedPatterns {
|
|
184
|
+
val contains = mutableListOf<String>()
|
|
185
|
+
val prefix = mutableListOf<String>()
|
|
186
|
+
val suffix = mutableListOf<String>()
|
|
187
|
+
|
|
188
|
+
allowedUrls.forEach { pattern ->
|
|
189
|
+
if (pattern.isEmpty()) return@forEach
|
|
190
|
+
|
|
191
|
+
val startsWithWildcard = pattern.startsWith('*')
|
|
192
|
+
val endsWithWildcard = pattern.endsWith('*')
|
|
193
|
+
|
|
194
|
+
when {
|
|
195
|
+
startsWithWildcard && endsWithWildcard -> {
|
|
196
|
+
// *text* -> contains check
|
|
197
|
+
if (pattern.length <= 2) return@forEach // Pattern is just * or ** - ignore
|
|
198
|
+
val extracted = pattern.substring(1, pattern.length - 1).lowercase()
|
|
199
|
+
if (extracted.isNotEmpty()) {
|
|
200
|
+
contains.add(extracted)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
endsWithWildcard -> {
|
|
204
|
+
// text* -> prefix check
|
|
205
|
+
if (pattern.length <= 1) return@forEach // Pattern is just * - ignore
|
|
206
|
+
val extracted = pattern.substring(0, pattern.length - 1).lowercase()
|
|
207
|
+
if (extracted.isNotEmpty()) {
|
|
208
|
+
prefix.add(extracted)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
startsWithWildcard -> {
|
|
212
|
+
// *text -> suffix check
|
|
213
|
+
if (pattern.length <= 1) return@forEach // Pattern is just * - ignore
|
|
214
|
+
val extracted = pattern.substring(1).lowercase()
|
|
215
|
+
if (extracted.isNotEmpty()) {
|
|
216
|
+
suffix.add(extracted)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else -> {
|
|
220
|
+
// No wildcard -> treat as contains check
|
|
221
|
+
val extracted = pattern.lowercase()
|
|
222
|
+
if (extracted.isNotEmpty()) {
|
|
223
|
+
contains.add(extracted)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return CategorizedPatterns(contains, prefix, suffix)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -2,43 +2,6 @@ package io.yourname.androidproject
|
|
|
2
2
|
|
|
3
3
|
import android.net.Uri
|
|
4
4
|
|
|
5
|
-
fun isUrlAllowed(url: String, allowedUrls: List<String>): Boolean {
|
|
6
|
-
// TODO: temp
|
|
7
|
-
return true
|
|
8
|
-
// Always allow framework server URLs (localhost with /framework- pattern)
|
|
9
|
-
// These are internal infrastructure URLs used for large file handling
|
|
10
|
-
// Use strict regex to prevent bypass attacks
|
|
11
|
-
val frameworkUrlPattern = Regex("^https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?/framework-[a-zA-Z0-9_-]+", RegexOption.IGNORE_CASE)
|
|
12
|
-
if (frameworkUrlPattern.containsMatchIn(url)) {
|
|
13
|
-
return true
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (allowedUrls.isEmpty()) return false
|
|
17
|
-
|
|
18
|
-
val parsedUrl = Uri.parse(url) ?: return false
|
|
19
|
-
val urlHost = "${parsedUrl.scheme}://${parsedUrl.host}${if (parsedUrl.port != -1) ":${parsedUrl.port}" else ""}"
|
|
20
|
-
|
|
21
|
-
return allowedUrls.any { pattern ->
|
|
22
|
-
when {
|
|
23
|
-
pattern.contains("*") -> {
|
|
24
|
-
val regex = pattern
|
|
25
|
-
.replace(".", "\\.")
|
|
26
|
-
.replace("*", ".*")
|
|
27
|
-
.toRegex(RegexOption.IGNORE_CASE)
|
|
28
|
-
regex.matches(url) || regex.matches(urlHost)
|
|
29
|
-
}
|
|
30
|
-
else -> {
|
|
31
|
-
val patternUri = Uri.parse(pattern)
|
|
32
|
-
val patternHost = "${patternUri.scheme}://${patternUri.host}${if (patternUri.port != -1) ":${patternUri.port}" else ""}"
|
|
33
|
-
|
|
34
|
-
// Exact match or pattern without port matches URL with any port
|
|
35
|
-
urlHost.equals(patternHost, ignoreCase = true) ||
|
|
36
|
-
(patternUri.port == -1 && urlHost.startsWith("${patternUri.scheme}://${patternUri.host}", ignoreCase = true))
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
5
|
/**
|
|
43
6
|
* Checks if a URL matches any of the cache patterns
|
|
44
7
|
* Handles query parameters, hashes, and version strings
|
|
@@ -92,38 +55,4 @@ fun matchesCachePattern(url: String, cachePatterns: List<String>): Boolean {
|
|
|
92
55
|
// If parsing fails, return false
|
|
93
56
|
return false
|
|
94
57
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fun isExternalDomain(url: String, allowedUrls: List<String>): Boolean {
|
|
98
|
-
if (allowedUrls.isEmpty()) return true
|
|
99
|
-
|
|
100
|
-
val parsedUrl = Uri.parse(url)
|
|
101
|
-
val urlHost = parsedUrl.host ?: return true
|
|
102
|
-
val urlScheme = parsedUrl.scheme ?: return true
|
|
103
|
-
val urlPort = parsedUrl.port
|
|
104
|
-
|
|
105
|
-
return !allowedUrls.any { pattern ->
|
|
106
|
-
if (pattern.startsWith("*.")) {
|
|
107
|
-
val domain = pattern.substring(2)
|
|
108
|
-
urlHost.equals(domain, ignoreCase = true) || urlHost.endsWith(".$domain", ignoreCase = true)
|
|
109
|
-
} else {
|
|
110
|
-
val patternUri = Uri.parse(pattern)
|
|
111
|
-
val patternHost = patternUri?.host ?: return@any false
|
|
112
|
-
val patternScheme = patternUri?.scheme ?: return@any false
|
|
113
|
-
val patternPort = patternUri?.port ?: -1
|
|
114
|
-
|
|
115
|
-
val schemeMatches = urlScheme.equals(patternScheme, ignoreCase = true)
|
|
116
|
-
val hostMatches = if (patternHost.startsWith("*.")) {
|
|
117
|
-
val domain = patternHost.substring(2)
|
|
118
|
-
urlHost.equals(domain, ignoreCase = true) || urlHost.endsWith(".$domain", ignoreCase = true)
|
|
119
|
-
} else {
|
|
120
|
-
urlHost.equals(patternHost, ignoreCase = true)
|
|
121
|
-
}
|
|
122
|
-
val portMatches = patternPort == -1 || urlPort == patternPort ||
|
|
123
|
-
(urlPort == -1 && ((patternPort == 443 && patternScheme.equals("https", ignoreCase = true)) ||
|
|
124
|
-
(patternPort == 80 && patternScheme.equals("http", ignoreCase = true))))
|
|
125
|
-
|
|
126
|
-
schemeMatches && hostMatches && portMatches
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
58
|
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt
CHANGED
|
@@ -178,6 +178,18 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
fun clearAll() {
|
|
182
|
+
try {
|
|
183
|
+
memoryCache.evictAll()
|
|
184
|
+
cacheDir.listFiles()?.forEach { it.delete() }
|
|
185
|
+
if (BuildConfig.DEBUG) {
|
|
186
|
+
Log.d(TAG, "clearAll: memory cache and disk cache wiped")
|
|
187
|
+
}
|
|
188
|
+
} catch (e: Exception) {
|
|
189
|
+
Log.e(TAG, "clearAll failed: ${e.message}")
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
suspend fun cleanup() {
|
|
182
194
|
withContext(Dispatchers.IO) {
|
|
183
195
|
try {
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt
CHANGED
|
@@ -75,6 +75,13 @@ object BridgeUtils {
|
|
|
75
75
|
NETWORK_STATUS_CHANGED("NETWORK_STATUS_CHANGED"),
|
|
76
76
|
// Safe area events
|
|
77
77
|
ON_SAFE_AREA_INSETS_UPDATED("ON_SAFE_AREA_INSETS_UPDATED"),
|
|
78
|
+
|
|
79
|
+
// Security events
|
|
80
|
+
ON_SCREEN_SECURE_SET("ON_SCREEN_SECURE_SET"),
|
|
81
|
+
ON_SCREEN_SECURE_STATUS("ON_SCREEN_SECURE_STATUS"),
|
|
82
|
+
ON_SCREEN_SECURE_ERROR("ON_SCREEN_SECURE_ERROR"),
|
|
83
|
+
ON_WEB_DATA_CLEARED("ON_WEB_DATA_CLEARED"),
|
|
84
|
+
ON_WEB_DATA_CLEAR_ERROR("ON_WEB_DATA_CLEAR_ERROR"),
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
/**
|
|
@@ -10,6 +10,7 @@ import java.io.IOException
|
|
|
10
10
|
import java.net.URL
|
|
11
11
|
import android.os.Handler
|
|
12
12
|
import android.os.Looper
|
|
13
|
+
import io.yourname.androidproject.URLWhitelistManager
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Download utilities for native bridge operations
|
|
@@ -80,13 +81,13 @@ object DownloadUtils {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
// Whitelist validation for remote URLs
|
|
83
|
-
if (
|
|
84
|
+
if (!URLWhitelistManager.isUrlAllowed(fileUrl)) {
|
|
84
85
|
val errorMessage = "Unable to process request. URL violates whitelisting protocols"
|
|
85
86
|
BridgeUtils.logError(TAG, "File URL blocked by access control: $fileUrl")
|
|
86
87
|
throw SecurityException(errorMessage)
|
|
87
|
-
} else if (
|
|
88
|
+
} else if (URLWhitelistManager.isAccessControlEnabled()) {
|
|
88
89
|
BridgeUtils.logDebug(TAG, "✅ File URL passed whitelist check: $fileUrl")
|
|
89
|
-
} else
|
|
90
|
+
} else {
|
|
90
91
|
BridgeUtils.logDebug(TAG, "⚙️ Access control disabled; skipping whitelist checks for download")
|
|
91
92
|
}
|
|
92
93
|
|