package com.igo.user.extension
|
|
import android.util.Log
|
import android.view.View
|
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceResponse
|
import android.webkit.WebView
|
import android.webkit.WebViewClient
|
import com.igo.user.utils.HtmlSanitizer
|
import java.net.HttpURLConnection
|
import java.net.URL
|
|
//扩展函数 设置防重复点击监听器
|
fun View.setOnDurationClickListener(duration: Long = 1000L, listener: View.OnClickListener) {
|
var lastClickTimestamp = 0L
|
setOnClickListener {
|
val now = System.currentTimeMillis()
|
if (now - lastClickTimestamp > duration) {
|
lastClickTimestamp = now
|
listener.onClick(it)
|
}
|
}
|
}
|
|
//扩展函数 网页加载控件安全设置
|
fun WebView.setSafeSetting() {
|
apply {
|
settings.allowFileAccess = false //设置不可以访问文件
|
webViewClient = object : WebViewClient() {
|
override fun shouldInterceptRequest(
|
view: WebView,
|
request: WebResourceRequest
|
): WebResourceResponse? {
|
val url = request.url.toString()
|
//访问文件危险操作
|
val dangerousOperation = listOf("file://", "content://", "android_asset")
|
// 阻止文件访问
|
if (dangerousOperation.any { url.contains(it, ignoreCase = true) }) {
|
return WebResourceResponse("text/plain", "UTF-8", 403, "Forbidden", emptyMap(), "".byteInputStream())
|
}
|
// 只对HTML文档添加CSP
|
if (isHtmlDocument(request)) {
|
return getResponseWithCSP(request)
|
}
|
return super.shouldInterceptRequest(view, request)
|
}
|
|
private fun isHtmlDocument(request: WebResourceRequest): Boolean {
|
val url = request.url.toString()
|
val contentType = request.requestHeaders?.get("Accept")
|
val urlPatterns = listOf(".html", ".htm", ".php", ".jsp", ".asp")
|
|
return urlPatterns.any { url.endsWith(it) } ||
|
contentType?.contains("text/html") == true
|
}
|
|
private fun getResponseWithCSP(request: WebResourceRequest): WebResourceResponse? {
|
try {
|
val urlConnection =
|
URL(request.url.toString()).openConnection() as HttpURLConnection
|
|
// 设置请求头
|
request.requestHeaders?.forEach { (key, value) ->
|
urlConnection.setRequestProperty(key, value)
|
}
|
|
urlConnection.requestMethod = request.method
|
val responseCode = urlConnection.responseCode
|
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
val mimeType =
|
urlConnection.contentType?.split(";")?.firstOrNull() ?: "text/html"
|
val encoding = urlConnection.contentEncoding ?: "UTF-8"
|
val inputStream = urlConnection.inputStream
|
|
// 创建带有CSP头的响应
|
val headers = mutableMapOf<String, String>()
|
|
// 收集原始响应头
|
urlConnection.headerFields.forEach { (key, valueList) ->
|
if (key != null && valueList != null && valueList.isNotEmpty()) {
|
headers[key] = valueList.joinToString(", ")
|
}
|
}
|
|
// 添加或覆盖CSP头
|
headers["Content-Security-Policy"] = buildCSPPolicy()
|
|
return WebResourceResponse(
|
mimeType, encoding, responseCode,
|
urlConnection.responseMessage, headers, inputStream
|
)
|
}
|
} catch (e: Exception) {
|
e.printStackTrace()
|
Log.e("CSP", "Failed to add CSP header")
|
}
|
return null
|
}
|
|
private fun buildCSPPolicy(): String {
|
return """
|
default-src 'self';
|
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted-cdn.com;
|
style-src 'self' 'unsafe-inline' https://trusted-cdn.com;
|
img-src 'self' data: https:;
|
font-src 'self' https://fonts.gstatic.com;
|
connect-src 'self' https://api.example.com;
|
frame-src 'self' https://trusted-iframe.com;
|
report-uri /csp-report-endpoint
|
""".trimIndent().replace("\n", " ")
|
}
|
|
override fun onPageFinished(view: WebView?, url: String?) {
|
super.onPageFinished(view, url)
|
// 注入安全监控脚本
|
injectSecurityMonitor(view)
|
}
|
|
private fun injectSecurityMonitor(view: WebView?) {
|
val securityScript = """
|
(function() {
|
'use strict';
|
|
// 监控DOM变化
|
const observer = new MutationObserver(function(mutations) {
|
mutations.forEach(function(mutation) {
|
mutation.addedNodes.forEach(function(node) {
|
if (node.nodeName === 'SCRIPT') {
|
console.warn('Dynamic script added:', node.src || node.textContent.substring(0, 100));
|
}
|
if (node.nodeName === 'IFRAME') {
|
console.warn('Dynamic iframe added:', node.src);
|
}
|
});
|
});
|
});
|
|
observer.observe(document.documentElement, {
|
childList: true,
|
subtree: true
|
});
|
|
// 拦截危险API调用
|
const originalFetch = window.fetch;
|
window.fetch = function() {
|
const url = arguments[0];
|
if (typeof url === 'string') {
|
if (url.startsWith('file://') || url.includes('localhost')) {
|
console.error('Blocked fetch to local file:', url);
|
return Promise.reject(new Error('Access to local files is blocked'));
|
}
|
}
|
return originalFetch.apply(this, arguments);
|
};
|
|
// 监控表单提交
|
document.addEventListener('submit', function(e) {
|
const form = e.target;
|
const action = form.getAttribute('action') || '';
|
if (action.startsWith('file://')) {
|
e.preventDefault();
|
console.error('Blocked form submission to file:', action);
|
}
|
});
|
})();
|
""".trimIndent()
|
view?.evaluateJavascript(securityScript, null)
|
}
|
}
|
}
|
}
|
|
//扩展函数 安全加载本地数据
|
fun WebView.safeLoadDataWithBaseURL(
|
baseUrl: String?,
|
data: String,
|
mimeType: String?,
|
encoding: String?,
|
historyUrl: String?
|
) {
|
loadDataWithBaseURL(
|
baseUrl,
|
HtmlSanitizer().sanitizeUserContent(data),
|
mimeType,
|
encoding,
|
historyUrl
|
)
|
}
|