18034727885
2026-09-18 1328a7b0b90825b1fd63bca8f7675e52df8b151d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
    )
}