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() // 收集原始响应头 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 ) }