├── app
├── .gitignore
├── src
│ └── main
│ │ ├── assets
│ │ ├── xposed_init
│ │ ├── editor.css
│ │ ├── devtools.js
│ │ ├── extension.js
│ │ ├── eruda.css
│ │ ├── editor.js
│ │ ├── encoding.js
│ │ └── scripts.js
│ │ ├── java
│ │ └── org
│ │ │ └── matrix
│ │ │ └── chromext
│ │ │ ├── hook
│ │ │ ├── Base.kt
│ │ │ ├── PageInfo.kt
│ │ │ ├── WebView.kt
│ │ │ ├── Preference.kt
│ │ │ ├── UserScript.kt
│ │ │ └── ContextMenu.kt
│ │ │ ├── proxy
│ │ │ ├── PageMenu.kt
│ │ │ ├── PageInfo.kt
│ │ │ ├── UserScript.kt
│ │ │ └── Preference.kt
│ │ │ ├── utils
│ │ │ ├── Log.kt
│ │ │ ├── Reflect.kt
│ │ │ ├── Hook.kt
│ │ │ ├── Url.kt
│ │ │ └── XMLHttpRequest.kt
│ │ │ ├── script
│ │ │ ├── SQLite.kt
│ │ │ ├── Parser.kt
│ │ │ ├── Local.kt
│ │ │ └── Manager.kt
│ │ │ ├── devtools
│ │ │ ├── Inspect.kt
│ │ │ └── WebSocketClient.kt
│ │ │ ├── OpenInChrome.kt
│ │ │ ├── extension
│ │ │ └── LocalFiles.kt
│ │ │ ├── MainHook.kt
│ │ │ └── Chrome.kt
│ │ ├── res
│ │ ├── mipmap
│ │ │ └── ic_launcher.xml
│ │ ├── mipmap-v26
│ │ │ └── ic_launcher.xml
│ │ ├── drawable
│ │ │ ├── ic_devtools.xml
│ │ │ ├── ic_extension.xml
│ │ │ ├── ic_install_script.xml
│ │ │ ├── ic_book.xml
│ │ │ └── ic_chrome.xml
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ └── arrays.xml
│ │ ├── menu
│ │ │ └── main_menu.xml
│ │ └── xml
│ │ │ └── developer_preferences.xml
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── .gitattributes
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .github
├── dependabot.yml
└── workflows
│ └── android.yml
├── settings.gradle.kts
├── gradle.properties
├── gradlew.bat
├── docs
└── presentation.tex
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | app/src/main/assets/*.js text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | .gradle
3 | .kotlin
4 | apktool
5 | *.apk
6 |
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | org.matrix.chromext.MainHook
2 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class org.matrix.chromext.MainHook
2 | -keepattributes SourceFile,LineNumberTable
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JingMatrix/ChromeXt/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/hook/Base.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.hook
2 |
3 | abstract class BaseHook {
4 | var isInit: Boolean = false
5 |
6 | abstract fun init()
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
[\S\s]*)""")
14 | private val metaReg = Regex("""^//\s+@(?[\w-]+)(\s+(?.+))?""")
15 |
16 | fun parseScript(input: String, storage: String? = null): Script? {
17 | val blockMatchGroup = blocksReg.matchEntire(input)?.groups
18 | if (blockMatchGroup == null) {
19 | return null
20 | }
21 |
22 | val script =
23 | object {
24 | var name = "sample"
25 | var namespace = "ChromeXt"
26 | var match = mutableListOf()
27 | var grant = mutableListOf()
28 | var exclude = mutableListOf()
29 | var require = mutableListOf()
30 | val meta = (blockMatchGroup[1]?.value as String)
31 | val code = blockMatchGroup[2]?.value as String
32 | var storage: JSONObject? = null
33 | var noframes = false
34 | }
35 | script.meta.split("\n").forEach {
36 | val metaMatchGroup = metaReg.matchEntire(it)?.groups
37 | if (metaMatchGroup != null) {
38 | val key = metaMatchGroup[1]?.value as String
39 | if (metaMatchGroup[2] != null) {
40 | val value = metaMatchGroup[3]?.value as String
41 | when (key) {
42 | "name" -> script.name = value.replace(":", "")
43 | "namespace" -> script.namespace = value
44 | "match" -> script.match.add(value)
45 | "include" -> script.match.add(value)
46 | "grant" -> script.grant.add(value)
47 | "exclude" -> script.exclude.add(value)
48 | "require" -> script.require.add(value)
49 | "noframes" -> script.noframes = true
50 | }
51 | } else {
52 | when (key) {
53 | "noframes" -> script.noframes = true
54 | }
55 | }
56 | }
57 | }
58 |
59 | if (!script.grant.contains("GM_xmlhttpRequest") &&
60 | (script.grant.contains("GM_download") ||
61 | script.grant.contains("GM.xmlHttpRequest") ||
62 | script.grant.contains("GM_getResourceText"))) {
63 | script.grant.add("GM_xmlhttpRequest")
64 | }
65 |
66 | if (script.grant.contains("GM.getValue") ||
67 | script.grant.contains("GM_getValue") ||
68 | script.grant.contains("GM_cookie")) {
69 | runCatching { script.storage = JSONObject(storage!!) }
70 | .onFailure { script.storage = JSONObject() }
71 | }
72 |
73 | if (script.match.size == 0) {
74 | return null
75 | } else {
76 | val lib = mutableListOf()
77 | Chrome.IO.submit { script.require.forEach { runCatching { lib.add(downloadLib(it)) } } }
78 | val parsed =
79 | Script(
80 | script.namespace + ":" + script.name,
81 | script.match.toTypedArray(),
82 | script.grant.toTypedArray(),
83 | script.exclude.toTypedArray(),
84 | script.meta,
85 | script.code,
86 | script.storage,
87 | lib,
88 | script.noframes)
89 | return parsed
90 | }
91 | }
92 |
93 | private fun downloadLib(libUrl: String): String {
94 | if (libUrl.startsWith("data:")) {
95 | val chunks = libUrl.split(",").toMutableList()
96 | val type = chunks.removeFirst()
97 | val data = Uri.decode(chunks.joinToString(""))
98 | if (type.endsWith("base64")) {
99 | return Base64.decode(data, Base64.DEFAULT).toString()
100 | } else {
101 | return data
102 | }
103 | }
104 | val url = URL(libUrl)
105 | val connection = url.openConnection() as HttpURLConnection
106 | return connection.inputStream.bufferedReader().use { it.readText() }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/assets/eruda.css:
--------------------------------------------------------------------------------
1 | /* font_fix */
2 | [class^="eruda-icon"]:before {
3 | font-size: 10px;
4 | display: block;
5 | }
6 | .eruda-icon-arrow-left:before {
7 | content: "←";
8 | }
9 | .eruda-icon-arrow-right:before {
10 | content: "→";
11 | }
12 | .eruda-icon-clear:before {
13 | content: "⦸";
14 | font-size: 16px;
15 | margin-left: 1px;
16 | }
17 | .eruda-icon-compress:before {
18 | content: "🗎";
19 | }
20 | .eruda-icon-copy:before,
21 | .luna-text-viewer-icon-copy:before {
22 | content: "⎘ ";
23 | font-size: 14px;
24 | font-weight: bold;
25 | }
26 | .eruda-icon-delete:before {
27 | content: "X";
28 | font-size: 15px;
29 | font-weight: bold;
30 | margin-left: 2px;
31 | transform: scale(1.3, 1);
32 | }
33 | .eruda-icon-expand:before {
34 | content: "⌄";
35 | }
36 | .eruda-icon-eye:before {
37 | content: "⊕";
38 | font-size: 16px;
39 | transform: translate(-2px, 0.5px);
40 | }
41 | #eruda-info .eruda-userscripts > h2 > span.eruda-icon-eye {
42 | transform: translate(0px, -6px);
43 | }
44 | div.eruda-btn.eruda-search {
45 | margin-top: 4px;
46 | }
47 | .eruda-icon-filter:before {
48 | content: "Ȳ";
49 | font-size: 13px;
50 | margin-left: 6px;
51 | transform: scale(1.5, 1);
52 | }
53 | .eruda-icon-play:before {
54 | content: "▷";
55 | }
56 | .eruda-icon-record:before {
57 | content: "●";
58 | }
59 | .eruda-icon-refresh:before {
60 | content: "↻";
61 | font-size: 17px;
62 | font-weight: normal;
63 | transform: translate(-5px, -1.5px);
64 | }
65 | .eruda-icon-reset:before {
66 | content: "↺";
67 | font-size: 18px;
68 | font-weight: bold;
69 | transform: rotate(270deg) translate(7px, 0);
70 | }
71 | .eruda-icon-search:before {
72 | content: "🔍";
73 | }
74 | .eruda-icon-select:before {
75 | content: "➤";
76 | font-size: 14px;
77 | transform: rotate(232deg);
78 | }
79 | .eruda-icon-tool:before {
80 | content: "⚙";
81 | font-size: 30px;
82 | }
83 | .luna-console-icon-error:before {
84 | content: "✗";
85 | }
86 | .luna-console-icon-warn:before {
87 | content: "⚠";
88 | }
89 | [class$="icon-caret-right"]:before,
90 | [class$="icon-arrow-right"]:before {
91 | content: "▼";
92 | font-size: 9px;
93 | display: block;
94 | transform: rotate(-0.25turn);
95 | }
96 | [class$="icon-caret-down"]:before,
97 | [class$="icon-arrow-down"]:before {
98 | content: "▼";
99 | font-size: 9px;
100 | }
101 |
102 | /* new_icons */
103 | .eruda-icon-add:before {
104 | content: "➕";
105 | font-size: 10px;
106 | vertical-align: 3px;
107 | }
108 | .eruda-icon-save:before {
109 | content: "💾";
110 | font-size: 10px;
111 | vertical-align: 3px;
112 | }
113 |
114 | /* dom_fix */
115 | #eruda-elements div.eruda-dom-viewer-container {
116 | overflow-x: hidden;
117 | }
118 | #eruda-elements div.eruda-dom-viewer-container > div.eruda-dom-viewer {
119 | overflow-x: scroll;
120 | }
121 | .luna-dom-viewer {
122 | min-width: 80vw;
123 | }
124 |
125 | /* plugin */
126 | .eruda-filters > ul > li {
127 | display: flex;
128 | }
129 | h2.eruda-title {
130 | width: 100%;
131 | }
132 | #eruda-info li .eruda-title span {
133 | padding-left: 8px;
134 | float: right;
135 | }
136 | #eruda-info .eruda-csp-rules > div.eruda-content {
137 | white-space: pre-wrap;
138 | margin-right: 3em;
139 | }
140 | #eruda-info .eruda-user-agent > h2 > span.eruda-reset {
141 | position: relative;
142 | top: 35px;
143 | left: 20.5px;
144 | }
145 | #eruda-info .eruda-userscripts > h2 > span.eruda-icon-eye {
146 | position: relative;
147 | right: 10px;
148 | }
149 | #eruda-info .eruda-user-agent > div.eruda-content {
150 | text-wrap: balance;
151 | text-align: center;
152 | margin-right: 3em;
153 | }
154 | #eruda-info .eruda-user-agent h2,
155 | #eruda-info .eruda-csp-rules h2 {
156 | padding-bottom: 12px;
157 | }
158 | #eruda-info .eruda-check-update {
159 | text-align: center;
160 | }
161 | #eruda-info .eruda-userscripts div.eruda-content,
162 | #eruda-resources div.eruda-commands {
163 | display: flex;
164 | flex-wrap: wrap;
165 | justify-content: space-around;
166 | > span {
167 | padding: 0.3em;
168 | margin: 0.3em;
169 | border: 0.5px solid violet;
170 | }
171 | }
172 | #eruda-resources .eruda-filter-item {
173 | width: 90%;
174 | padding: 0.3em;
175 | }
176 | #eruda-resources .eruda-delete-filter {
177 | width: 10%;
178 | margin: auto;
179 | text-align: center;
180 | }
181 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/hook/WebView.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.hook
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import android.os.Handler
6 | import java.lang.ref.WeakReference
7 | import org.matrix.chromext.Chrome
8 | import org.matrix.chromext.Listener
9 | import org.matrix.chromext.script.Local
10 | import org.matrix.chromext.script.ScriptDbManager
11 | import org.matrix.chromext.utils.Log
12 | import org.matrix.chromext.utils.findField
13 | import org.matrix.chromext.utils.findMethod
14 | import org.matrix.chromext.utils.hookAfter
15 | import org.matrix.chromext.utils.hookBefore
16 | import org.matrix.chromext.utils.invokeMethod
17 |
18 | object WebViewHook : BaseHook() {
19 |
20 | var ViewClient: Class<*>? = null
21 | var ChromeClient: Class<*>? = null
22 | var WebView: Class<*>? = null
23 | val records = mutableListOf>()
24 |
25 | fun evaluateJavascript(code: String?, view: Any?) {
26 | val webView = (view ?: Chrome.getTab())
27 | if (code != null && code.length > 0 && webView != null) {
28 | val webSettings = webView.invokeMethod { name == "getSettings" }
29 | if (webSettings?.invokeMethod { name == "getJavaScriptEnabled" } == true)
30 | Handler(Chrome.getContext().mainLooper).post {
31 | webView.invokeMethod(code, null) { name == "evaluateJavascript" }
32 | }
33 | }
34 | }
35 |
36 | override fun init() {
37 |
38 | findMethod(ChromeClient!!, true) { name == "onConsoleMessage" && parameterCount == 1 }
39 | // public boolean onConsoleMessage (ConsoleMessage consoleMessage)
40 | .hookAfter {
41 | // Don't use ConsoleMessage to specify this method since Mi Browser uses its own
42 | // implementation
43 | // This should be the way to communicate with the front-end of ChromeXt
44 | val chromeClient = it.thisObject
45 | val consoleMessage = it.args[0]
46 | val messageLevel = consoleMessage.invokeMethod { name == "messageLevel" }
47 | val sourceId = consoleMessage.invokeMethod { name == "sourceId" } as String
48 | val lineNumber = consoleMessage.invokeMethod { name == "lineNumber" }
49 | val message = consoleMessage.invokeMethod { name == "message" } as String
50 | if (messageLevel.toString() == "TIP" &&
51 | sourceId.startsWith("local://ChromeXt/init") &&
52 | lineNumber == Local.anchorInChromeXt) {
53 | val webView =
54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
55 | records
56 | .find {
57 | if (Chrome.isQihoo) {
58 | val mProvider = findField(WebView!!) { name == "mProvider" }
59 | mProvider.get(it.get())
60 | } else {
61 | it.get()
62 | }
63 | ?.invokeMethod { name == "getWebChromeClient" } == chromeClient
64 | }
65 | ?.get()
66 | } else Chrome.getTab()
67 | Listener.startAction(message, webView, chromeClient, sourceId)
68 | } else {
69 | Log.d(messageLevel.toString() + ": [${sourceId}@${lineNumber}] ${message}")
70 | }
71 | }
72 |
73 | fun onUpdateUrl(url: String, view: Any?) {
74 | if (url.startsWith("javascript") || view == null) return
75 | Chrome.updateTab(view)
76 | ScriptDbManager.invokeScript(url, view)
77 | }
78 |
79 | findMethod(WebView!!) { name == "setWebChromeClient" }
80 | .hookAfter {
81 | val webView = it.thisObject
82 | records.removeAll(records.filter { it.get() == null || it.get() == webView })
83 | if (it.args[0] != null) records.add(WeakReference(webView))
84 | }
85 |
86 | findMethod(WebView!!) { name == "onAttachedToWindow" }
87 | .hookAfter { Chrome.updateTab(it.thisObject) }
88 |
89 | findMethod(ViewClient!!, true) { name == "onPageStarted" }
90 | // public void onPageStarted (WebView view, String url, Bitmap favicon)
91 | .hookAfter {
92 | if (Chrome.isQihoo && it.thisObject::class.java.declaredMethods.size > 1) return@hookAfter
93 | onUpdateUrl(it.args[1] as String, it.args[0])
94 | }
95 |
96 | findMethod(Activity::class.java) { name == "onStop" }
97 | .hookBefore { ScriptDbManager.updateScriptStorage() }
98 | isInit = true
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/utils/Url.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.utils
2 |
3 | import android.net.Uri
4 | import android.provider.OpenableColumns
5 | import kotlin.text.Regex
6 | import org.matrix.chromext.Chrome
7 | import org.matrix.chromext.script.Script
8 |
9 | const val ERUD_URL = "https://cdn.jsdelivr.net/npm/eruda"
10 | private const val DEV_FRONT_END = "https://chrome-devtools-frontend.appspot.com"
11 |
12 | fun randomString(length: Int): String {
13 | val alphabet: List = ('a'..'z') + ('A'..'Z')
14 | return List(length) { alphabet.random() }.joinToString("")
15 | }
16 |
17 | private fun urlMatch(match: String, url: String, strict: Boolean): Boolean {
18 | var pattern = match
19 | val regexPattern = pattern.startsWith("/") && pattern.endsWith("/")
20 |
21 | if (regexPattern) {
22 | pattern = pattern.removeSurrounding("/", "/")
23 | pattern = pattern.replace("\\/", "/")
24 | pattern = pattern.replace("\\://", "://")
25 | } else if ("*" !in pattern) {
26 | if (strict) {
27 | return pattern == url
28 | } else {
29 | return pattern in url
30 | }
31 | } else if ("://" in pattern || strict) {
32 | pattern = pattern.replace("?", "\\?")
33 | pattern = pattern.replace(".", "\\.")
34 | pattern = pattern.replace("*", "[^:]*")
35 | pattern = pattern.replace("[^:]*\\.", "([^:]*\\.)?")
36 | } else {
37 | return false
38 | }
39 |
40 | runCatching {
41 | val result =
42 | if (regexPattern) {
43 | Regex(pattern).containsMatchIn(url)
44 | } else {
45 | Regex(pattern).matches(url)
46 | }
47 | return result
48 | }
49 | .onFailure { Log.i("Invaid matching rule: ${match}, error: " + it.message) }
50 | return false
51 | }
52 |
53 | fun matching(script: Script, url: String): Boolean {
54 | script.exclude.forEach {
55 | if (urlMatch(it, url, true)) {
56 | return false
57 | }
58 | }
59 | script.match.forEach {
60 | if (urlMatch(it, url, false)) {
61 | // Log.d("${script.id} injected")
62 | return true
63 | }
64 | }
65 | return false
66 | }
67 |
68 | fun isDevToolsFrontEnd(url: String?): Boolean {
69 | if (url == null) return false
70 | return url.startsWith(DEV_FRONT_END)
71 | }
72 |
73 | private val invalidUserScriptDomains = listOf("github.com")
74 | val invalidUserScriptUrls = mutableListOf()
75 |
76 | fun isUserScript(url: String?, path: String? = null): Boolean {
77 | if (url == null) return false
78 | if (url.endsWith(".user.js") ||
79 | (Chrome.isEdge &&
80 | url.endsWith(".js") &&
81 | (url.startsWith("file://") || url.startsWith("content://")))) {
82 | if (invalidUserScriptUrls.contains(url)) return false
83 | invalidUserScriptDomains.forEach { if (url.startsWith("https://" + it) == true) return false }
84 | return true
85 | } else {
86 | return (path ?: resolveContentUrl(url)).endsWith(".js")
87 | }
88 | }
89 |
90 | fun resolveContentUrl(url: String): String {
91 | if (!url.startsWith("content://")) return ""
92 | Chrome.getContext().contentResolver.query(Uri.parse(url), null, null, null, null)?.use { cursor ->
93 | cursor.moveToFirst()
94 | val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
95 | val dataIndex = cursor.getColumnIndex("_data")
96 | if (dataIndex != -1) {
97 | return cursor.getString(dataIndex) ?: cursor.getString(nameIndex)
98 | } else {
99 | return cursor.getString(nameIndex)
100 | }
101 | }
102 | return ""
103 | }
104 |
105 | private val trustedHosts =
106 | listOf("jingmatrix.github.io", "jianyu-ma.onrender.com", "jianyu-ma.netlify.app")
107 |
108 | fun isChromeXtFrontEnd(url: String?): Boolean {
109 | if (url == null || !url.endsWith("/ChromeXt/")) return false
110 | trustedHosts.forEach { if (url == "https://" + it + "/ChromeXt/") return true }
111 | return false
112 | }
113 |
114 | private val sandboxHosts = listOf("raw.githubusercontent.com", "gist.githubusercontent.com")
115 |
116 | fun shouldBypassSandbox(url: String?): Boolean {
117 | sandboxHosts.forEach { if (url?.startsWith("https://" + it) == true) return true }
118 | return false
119 | }
120 |
121 | fun parseOrigin(url: String): String? {
122 | val protocol = url.split("://")
123 | if (protocol.size > 1 && arrayOf("https", "http", "file").contains(protocol.first())) {
124 | return protocol.first() + "://" + protocol[1].split("/").first()
125 | } else {
126 | return null
127 | }
128 | }
129 |
130 | fun isChromeScheme(url: String): Boolean {
131 | val protocol = url.split("://")
132 | return (protocol.size > 1 &&
133 | arrayOf("chrome", "chrome-native", "edge").contains(protocol.first()))
134 | }
135 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/extension/LocalFiles.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.extension
2 |
3 | import java.io.File
4 | import java.io.FileReader
5 | import java.net.ServerSocket
6 | import java.net.Socket
7 | import java.net.URLConnection
8 | import org.json.JSONArray
9 | import org.json.JSONObject
10 | import org.matrix.chromext.Chrome
11 | import org.matrix.chromext.utils.Log
12 |
13 | object LocalFiles {
14 |
15 | private val directory: File
16 | private val extensions = mutableMapOf()
17 | val script: String
18 |
19 | init {
20 | val ctx = Chrome.getContext()
21 | directory = File(ctx.getExternalFilesDir(null), "Extension")
22 | script = ctx.assets.open("extension.js").bufferedReader().use { it.readText() }
23 | if (!directory.exists()) directory.mkdirs()
24 | directory.listFiles()?.forEach {
25 | val path = it.name
26 | val manifest = File(it, "manifest.json")
27 | if (manifest.exists()) {
28 | val json = FileReader(manifest).use { it.readText() }
29 | runCatching { extensions.put(path, JSONObject(json)) }
30 | }
31 | }
32 | }
33 |
34 | private fun serveFiles(id: String, connection: Socket) {
35 | val path = directory.toString() + "/" + id
36 | val background = extensions.get(id)?.optJSONObject("background")?.optString("page")
37 | runCatching {
38 | connection.inputStream.bufferedReader().use {
39 | val requestLine = it.readLine()
40 | if (requestLine == null) {
41 | connection.close()
42 | return
43 | }
44 | val request = requestLine.split(" ")
45 | if (request[0] == "GET" && request[2] == "HTTP/1.1") {
46 | val name = request[1]
47 | val file = File(path + name)
48 | if (!file.exists() && name != "/ChromeXt.js") {
49 | connection.outputStream.write("HTTP/1.1 404 Not Found\r\n\r\n".toByteArray())
50 | } else if (file.isDirectory() || name.contains("..")) {
51 | connection.outputStream.write("HTTP/1.1 403 Forbidden\r\n\r\n".toByteArray())
52 | } else {
53 | val data =
54 | if (name == "/" + background) {
55 | val html = FileReader(file).use { it.readText() }
56 | ""
57 | .toByteArray()
58 | } else if (name == "/ChromeXt.js") {
59 | script.toByteArray()
60 | } else {
61 | file.readBytes()
62 | }
63 | val type = URLConnection.guessContentTypeFromName(name) ?: "text/plain"
64 | val response =
65 | arrayOf(
66 | "HTTP/1.1 200",
67 | "Content-Length: ${data.size}",
68 | "Content-Type: ${type}",
69 | "Access-Control-Allow-Origin: *")
70 | connection.outputStream.write(
71 | (response.joinToString("\r\n") + "\r\n\r\n").toByteArray())
72 | connection.outputStream.write(data)
73 | }
74 | connection.close()
75 | }
76 | }
77 | }
78 | .onFailure { Log.ex(it) }
79 | }
80 |
81 | private fun startServer(id: String) {
82 | if (extensions.containsKey(id) && !extensions.get(id)!!.has("port")) {
83 | val server = ServerSocket()
84 | server.bind(null)
85 | val port = server.getLocalPort()
86 | Log.d("Listening at port ${port} for ${id}")
87 | Chrome.IO.submit {
88 | runCatching {
89 | while (true) {
90 | val socket = server.accept()
91 | Chrome.IO.submit { serveFiles(id, socket) }
92 | }
93 | }
94 | .onFailure {
95 | Log.ex(it)
96 | server.close()
97 | if (extensions.get(id)?.optInt("port") == port) {
98 | extensions.get(id)!!.remove("port")
99 | }
100 | }
101 | }
102 | extensions.get(id)!!.put("port", server.getLocalPort())
103 | extensions.get(id)!!.put("tabUrl", Chrome.getUrl())
104 | }
105 | }
106 |
107 | fun start(): JSONObject {
108 | extensions.keys.forEach { startServer(it) }
109 | val info =
110 | if (extensions.keys.size == 0) {
111 | Log.d("No extensions found")
112 | JSONArray()
113 | } else {
114 | JSONArray(extensions.map { it.value.put("id", it.key) })
115 | }
116 | return JSONObject(mapOf("type" to "init", "manifests" to info))
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/app/src/main/assets/editor.js:
--------------------------------------------------------------------------------
1 | const isSandboxed = [
2 | "raw.githubusercontent.com",
3 | "gist.githubusercontent.com",
4 | ].includes(location.hostname);
5 |
6 | async function installScript(force = false) {
7 | const dialog = document.querySelector("dialog#confirm");
8 | if (!force) {
9 | dialog.showModal();
10 | } else {
11 | dialog.close();
12 | const script = document.body.innerText;
13 | Symbol.ChromeXt.dispatch("installScript", script);
14 | }
15 | }
16 |
17 | function renderEditor(code, alertEncoding) {
18 | let scriptMeta = document.querySelector("#meta");
19 | if (scriptMeta) return;
20 | const separator = "==/UserScript==\n";
21 | const script = code.innerHTML.split(separator);
22 | if (separator.length == 1) return;
23 | let html = (script.shift() + separator).replace(
24 | "GM.ChromeXt",
25 | "GM.ChromeXt"
26 | );
27 | for (const api of ["GM_notification", "GM_setClipboard", "GM_cookie"]) {
28 | html = html.replace(api, `${api}`);
29 | }
30 | scriptMeta = document.createElement("pre");
31 | scriptMeta.innerHTML = html;
32 | code.innerHTML = script.join(separator);
33 | code.id = "code";
34 | code.removeAttribute("style");
35 | scriptMeta.id = "meta";
36 | document.body.prepend(scriptMeta);
37 |
38 | if (alertEncoding) {
39 | const msg =
40 | "Current script may contain badly encoded text.\n\nTo fix possible issues, you can download this script and open it locally.";
41 | createDialog(msg, false);
42 | } else {
43 | const msg =
44 | "Code editor is blocked on this page.\n\nPlease use the menu to install this UserScript, or reload the page to solve this problem.";
45 | createDialog(msg);
46 | setTimeout(fixDialog);
47 | // setTimeout is not working in sandboxed pages, and thus can be used for detecting sandboxed pages
48 | }
49 |
50 | scriptMeta.setAttribute("contenteditable", true);
51 | code.setAttribute("contenteditable", true);
52 | scriptMeta.setAttribute("spellcheck", false);
53 | code.setAttribute("spellcheck", false);
54 | // Too many nodes heavily slow down the event-loop, should be improved
55 | import("https://unpkg.com/@speed-highlight/core/dist/index.js").then(
56 | (imports) => {
57 | imports.highlightElement(code, "js", "multiline", {
58 | hideLineNumbers: true,
59 | });
60 | }
61 | );
62 | }
63 |
64 | function createDialog(msg) {
65 | const dialog = document.createElement("dialog");
66 | dialog.id = "confirm";
67 | dialog.textContent = msg;
68 | document.body.prepend(dialog);
69 | dialog.show();
70 | }
71 |
72 | function fixDialog() {
73 | const dialog = document.querySelector("dialog#confirm");
74 | if (dialog.textContent == "") return;
75 | dialog.close();
76 | dialog.textContent = "";
77 | const text = document.createElement("p");
78 | text.textContent = "Confirm ChromeXt to install this UserScript?";
79 | const div = document.createElement("div");
80 | div.id = "interaction";
81 | const yes = document.createElement("button");
82 | yes.textContent = "Confirm";
83 | yes.addEventListener("click", () => installScript(true));
84 | const no = document.createElement("button");
85 | no.addEventListener("click", () => {
86 | dialog.close();
87 | setTimeout(() => dialog.show(), 30000);
88 | });
89 | no.textContent = "Ask 30s later";
90 | div.append(yes);
91 | div.append(no);
92 | dialog.append(text);
93 | const askChromeXt = document.querySelector("#meta > em") != undefined;
94 | if (askChromeXt) {
95 | const alert = document.createElement("p");
96 | alert.id = "alert";
97 | alert.textContent = "ATTENTION: GM.ChromeXt is declared";
98 | dialog.append(alert);
99 | }
100 | dialog.append(div);
101 | installScript();
102 | }
103 |
104 | async function prepareDOM() {
105 | if (Symbol.ChromeXt == undefined) return;
106 | if (document.querySelector("script,div,p") != null) return;
107 | const meta = document.createElement("meta");
108 | const style = document.createElement("style");
109 |
110 | style.setAttribute("type", "text/css");
111 | meta.setAttribute("name", "viewport");
112 | meta.setAttribute(
113 | "content",
114 | "width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
115 | );
116 | style.textContent = _editor_style;
117 |
118 | const code = document.querySelector("body > pre");
119 | if (document.readyState == "loading") {
120 | if (isSandboxed) {
121 | return prepareDOM();
122 | // EventListeners are unavailable in sandboxed pages
123 | } else {
124 | return document.addEventListener("DOMContentLoaded", prepareDOM);
125 | }
126 | }
127 | Symbol.installScript = installScript;
128 | document.head.appendChild(meta);
129 | document.head.appendChild(style);
130 |
131 | const alertEncoding = !(await fixEncoding(true, true, code));
132 | renderEditor(code, alertEncoding);
133 | }
134 |
135 | prepareDOM();
136 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/hook/Preference.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.hook
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.graphics.Insets
7 | import android.graphics.Rect
8 | import android.net.Uri
9 | import android.os.Build
10 | import android.os.Bundle
11 | import android.view.WindowInsets
12 | import java.lang.reflect.Modifier
13 | import kotlin.math.roundToInt
14 | import org.matrix.chromext.Chrome
15 | import org.matrix.chromext.R
16 | import org.matrix.chromext.Resource
17 | import org.matrix.chromext.proxy.PreferenceProxy
18 | import org.matrix.chromext.utils.*
19 |
20 | object PreferenceHook : BaseHook() {
21 |
22 | private fun getUrl(): String {
23 | return Chrome.getUrl()!!
24 | }
25 |
26 | override fun init() {
27 |
28 | val proxy = PreferenceProxy
29 |
30 | proxy.addPreferencesFromResource
31 | // public void addPreferencesFromResource(Int preferencesResId)
32 | .hookMethod {
33 | before {
34 | if (it.thisObject::class.java == proxy.developerSettings) {
35 | it.args[0] = R.xml.developer_preferences
36 | }
37 | }
38 |
39 | after {
40 | if (it.thisObject::class.java == proxy.developerSettings) {
41 | val refThis = it
42 | val preferences = mutableMapOf()
43 | arrayOf("eruda", "gesture_mod", "keep_storage", "bookmark", "reset", "exit").forEach {
44 | preferences[it] = proxy.findPreference.invoke(refThis.thisObject, it)!!
45 | }
46 | proxy.setClickListener(preferences.toMap())
47 | }
48 | }
49 | }
50 |
51 | findMethod(proxy.developerSettings, true) {
52 | Modifier.isStatic(modifiers) &&
53 | parameterTypes contentDeepEquals
54 | arrayOf(Context::class.java, String::class.java, Bundle::class.java)
55 | // public static Fragment instantiate(Context context,
56 | // String fname, @Nullable Bundle args)
57 | }
58 | .hookAfter {
59 | if (it.result::class.java == proxy.developerSettings) {
60 | Resource.enrich(it.args[0] as Context)
61 | }
62 | }
63 |
64 | findMethod(proxy.chromeTabbedActivity) { name == "onNewIntent" || name == "onMAMNewIntent" }
65 | .hookBefore {
66 | val intent = it.args[0] as Intent
67 | if (intent.hasExtra("ChromeXt")) {
68 | intent.setAction(Intent.ACTION_VIEW)
69 | var url = intent.getStringExtra("ChromeXt") as String
70 | intent.setData(Uri.parse(url))
71 | }
72 | }
73 |
74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
75 | findMethodOrNull(WindowInsets::class.java) { name == "getSystemGestureInsets" }
76 | ?.hookBefore {
77 | val ctx = Chrome.getContext()
78 | val sharedPref = ctx.getSharedPreferences("ChromeXt", Context.MODE_PRIVATE)
79 | if (sharedPref.getBoolean("gesture_mod", true)) {
80 | it.result = Insets.of(0, 0, 0, 0)
81 | toggleGestureConflict(true)
82 | } else {
83 | toggleGestureConflict(false)
84 | }
85 | }
86 | }
87 |
88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
89 | findMethod(WindowInsets::class.java) {
90 | name == "getInsets" &&
91 | parameterTypes.size == 1 &&
92 | parameterTypes.first() == Int::class.java
93 | }
94 | .hookBefore {
95 | val typeMask = it.args[0] as Int
96 | if (typeMask == WindowInsets.Type.systemGestures()) {
97 | val ctx = Chrome.getContext()
98 | val sharedPref = ctx.getSharedPreferences("ChromeXt", Context.MODE_PRIVATE)
99 | if (sharedPref.getBoolean("gesture_mod", true)) {
100 | it.result = Insets.of(0, 0, 0, 0)
101 | toggleGestureConflict(true)
102 | } else {
103 | toggleGestureConflict(false)
104 | }
105 | }
106 | }
107 | }
108 |
109 | isInit = true
110 | }
111 |
112 | private fun toggleGestureConflict(excludeSystemGesture: Boolean) {
113 | val activity = Chrome.getContext()
114 | if (activity is Activity && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
115 | val decoView = activity.window.decorView
116 | if (excludeSystemGesture) {
117 | val width = decoView.width
118 | val height = decoView.height
119 | val excludeHeight: Int = (activity.resources.displayMetrics.density * 100).roundToInt()
120 | decoView.setSystemGestureExclusionRects(
121 | // public Rect (int left, int top, int right, int bottom)
122 | listOf(Rect(width / 2, height / 2 - excludeHeight, width, height / 2 + excludeHeight)))
123 | } else {
124 | decoView.setSystemGestureExclusionRects(listOf(Rect(0, 0, 0, 0)))
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/docs/presentation.tex:
--------------------------------------------------------------------------------
1 | % ! TEX program = lualatex
2 | \documentclass[aspectratio=169]{beamer}
3 |
4 | \usepackage{emoji}
5 |
6 | \usefonttheme[onlymath]{serif}
7 | \beamertemplatenavigationsymbolsempty
8 | \setbeamertemplate{footline}[frame number]{}
9 | \setbeamertemplate{caption}{\raggedright\insertcaption\par}
10 | \newcommand{\diff}{\operatorname{d}}
11 | \newcommand{\info}[1]{\texttt{{\color{teal}(#1)}}}
12 |
13 | \title{
14 | Introduction to ChromeXt\\
15 | {\color{teal}\normalsize UserScript and DevTools supports for mobile browsers }\\
16 | }
17 | \author{\texttt{JingMatrix}}
18 | \date{\small \color{gray} \today}
19 |
20 | \begin{document}
21 | {
22 | \setbeamertemplate{footline}{}
23 | \begin{frame}[noframenumbering]
24 | \titlepage
25 | \end{frame}
26 | }
27 |
28 | \section{Background}
29 |
30 | \setbeamercovered{transparent}
31 | \begin{frame}
32 | \frametitle{What is ChromeXt?}
33 | \begin{definition}
34 | ChromeXt is {\color<2-5>{brown} a tiny Xposed module}
35 | \info{Android} that adds {\color<6-9>{brown} UserScript}, {\color<10-12>{brown} DevTools} and
36 | some other functions to {\color<13-15>{brown} Chromium based and WebView based mobile browsers}.
37 | \end{definition}
38 |
39 | \vspace{0.5cm}
40 | \uncover<2->{
41 | \begin{block}{{\only<2-5>{What is Xposed}\only<6-9>{What are UserScripts}\only<10-12>{What are DevTools}\only<13-15>{Which browsers are supported}}? }
42 | \only<2-15>{
43 | \begin{itemize}
44 | \only<2-5>{
45 | \item<2-> Xposed is a framework helping developers to modify Android applications.
46 | \item<3-> The most popular Xposed framework for Android $8.1 \sim 14$ is LSPosed.
47 | \item<4-> ChromeXt is an Xposed module with size less than $0.1$\,MB.
48 | \item<5-> Both ChromeXt and LSPosed are open source projects powered
49 | by \only<5>{\emoji{heart}}.
50 | }
51 |
52 | \only<6-9>{
53 | \item<6-> UserScripts enable users to modify websites in their browsers.
54 | \item<7-> UserScripts are supported by most desktop browsers but only few mobile browsers.
55 | \item<8-> Popular UserScript managers include Tampermonkey and Violentmonkey;
56 | and people usually share their UserScripts on GreasyFork or GitHub.
57 | \item<9-> ChromeXt works as a UserScript manager for mobile browsers.
58 | }
59 | \only<10-12>{
60 | \item<10-> DevTools help developers to inspect, modify and debug websites in browsers.
61 | \item<11-> They are the primary tools for hacking or finding vulnerabilities of websites.
62 | \item<12-> Usually one needs desktop browsers to inspect websites in mobile phones.
63 | }
64 | \only<13-15>{
65 | \item<13-> Chromium based browsers include Chrome, Edge, Bromite, Brave,
66 | Vivalid $\ldots$
67 | \item<14-> WebView based browsers include Via, Soul, FOSS Browser $\ldots$
68 | \item<15-> Not supported: Firefox, Samsung Internet Browser, Opera.
69 | }
70 | \end{itemize}
71 | }
72 | \end{block}
73 | }
74 | \end{frame}
75 |
76 | \section{Installation and Usage}
77 | \begin{frame}
78 | \frametitle{How to install ChromeXt}
79 | \begin{block}{Root users}
80 | Install LSPosed \info{Magisk module}
81 | and then install ChromeXt.
82 | \end{block}
83 | \vspace{1cm}
84 | \uncover<2->{
85 | \begin{block}{Non-root users}
86 | \begin{enumerate}
87 | \item<2-> Download LSPatch \info{modified by JingMatrix} and ChromeXt.
88 | \item<3-> If Java is available, then use lspatch.jar, otherwise install manager.apk.
89 | \item<4-> Patch the target browser to embed ChromeXt.apk.
90 | \end{enumerate}
91 | \end{block}
92 | }
93 | \end{frame}
94 |
95 | \begin{frame}
96 | \frametitle{How to use ChromeXt?}
97 | ChromeXt is fully integrated into the target browser, almost all interactions are
98 | done within the browser.
99 |
100 | \uncover<2->{
101 | \begin{block}{Different ways to install UserScripts}
102 | Open .user.js URLs, open local UserScripts with ChromeXt, import via
103 | Eruda console.
104 | \end{block}
105 | }
106 |
107 | \uncover<3->{
108 | \begin{block}{Functions offered by ChromeXt}
109 | \begin{itemize}
110 | \item<3-> Via front end: manage and modify installed UserScripts
111 | \item<4-> Via page menu: reader mode, Eruda console and Developer Tools
112 | \item<5-> Via Developer options setting: set gesture navigation, export bookmarks
113 | \item<6-> Via Eruda console: cosmetic filters, user-agent spoofing, UserScript commands
114 | \end{itemize}
115 | \end{block}
116 | }
117 | \end{frame}
118 |
119 | \section{Tutorial to hack YouTube services}
120 |
121 | \begin{frame}
122 | \frametitle{Tutorial: write a UserScript to remove YouTube advertisements}
123 | \begin{block}{Analysis}
124 | \begin{enumerate}
125 | \item<1-> Different videos contains different advertisements to be played at
126 | different time, but the YouTube page does not reload when we switch videos.
127 | \item<2-> Therefore, advertisement data are fetched from remote for each new video.
128 | \item<3-> Find the code that fetching remote advertisement data.
129 | \item<4-> Change the fetched data to clear all advertisement data.
130 | \end{enumerate}
131 | \end{block}
132 |
133 | \uncover<5->{
134 | \begin{block}{Start writing a UserScript}
135 | \begin{enumerate}
136 | \item Only partial code will be shown for instructive purpose.
137 | \item The previously described method is novel, no source code available online.
138 | \item YouTube Music has the same vulnerability.
139 | \end{enumerate}
140 | \end{block}
141 | }
142 | \end{frame}
143 |
144 | \end{document}
145 |
--------------------------------------------------------------------------------
/app/src/main/assets/encoding.js:
--------------------------------------------------------------------------------
1 | const invalidChar = "�";
2 |
3 | class Encoding {
4 | #name;
5 | decoder = new TextDecoder();
6 | get encoding() {
7 | return this.#name;
8 | }
9 | map = () => [];
10 | constructor(name = "utf-8") {
11 | this.#name = name.toLowerCase();
12 | }
13 | defaultOnError(_input, result) {
14 | result.push(0xff);
15 | }
16 | defaultOnAlloc = (data) => new Uint8Array(data);
17 | static generateTable() {
18 | return new Map();
19 | }
20 | encode(input, opt = {}) {
21 | if (!(this.table instanceof Map))
22 | Object.defineProperty(this, "table", {
23 | value: new Map([
24 | ...this.constructor.generateTable(this.decode.bind(this)),
25 | ...this.map(),
26 | ]),
27 | });
28 | if (this.encoding == "utf-8") return new TextEncoder().encode(input);
29 | const onError = opt.onError || this.defaultOnError.bind(this);
30 | const onAlloc = opt.onAlloc || this.defaultOnAlloc.bind(this);
31 | const result = [];
32 | [...input].forEach((str) => {
33 | let codePoint = str.codePointAt(0);
34 | if (0x00 <= codePoint && codePoint < 0x80) {
35 | result.push[codePoint];
36 | return;
37 | }
38 | if (this.table.has(codePoint)) {
39 | result.push(this.table.get(codePoint));
40 | } else if (str == invalidChar) {
41 | const ret = onError(input, result);
42 | if (ret === -1) {
43 | throw Error("Stop decoding", input);
44 | }
45 | }
46 | });
47 | return new Uint8Array(onAlloc(result).buffer).filter((c) => c != 0x00);
48 | }
49 | decode(uint8) {
50 | return new TextDecoder(this.#name).decode(uint8);
51 | }
52 | convert(text) {
53 | return this.decoder.decode(this.encode(text));
54 | }
55 | }
56 |
57 | class SingleByte extends Encoding {
58 | static generateTable(decode, start = 0x80, end = 0xff) {
59 | const range = [...Array(end - start + 1).keys()];
60 | const charCodes = new Uint8Array(range.map((x) => x + start));
61 | const str = decode(charCodes);
62 | console.assert(str.length == charCodes.length);
63 | return new Map(range.map((i) => [str.codePointAt(i), charCodes[i]]));
64 | }
65 | }
66 |
67 | class TwoBytes extends Encoding {
68 | static intervals = [[0x81, 0xfe, 0x40, 0xfe]];
69 | static generateTable(decode) {
70 | const map = [];
71 | this.intervals.forEach(([b1Begin, b1End, b2Begin, b2End]) => {
72 | for (let b1 = b1Begin; b1 <= b1End; b1++) {
73 | for (let b2 = b2Begin; b2 <= b2End; b2++) {
74 | const charCode = (b2 << 8) | b1;
75 | const str = decode(new Uint16Array([charCode]));
76 | if (!str.includes(invalidChar))
77 | map.push([str.codePointAt(0), charCode]);
78 | }
79 | }
80 | });
81 | return map;
82 | }
83 | defaultOnAlloc = (data) => new Uint16Array(data);
84 | }
85 |
86 | class GBK extends TwoBytes {
87 | // https://en.wikipedia.org/wiki/GBK_(character_encoding)
88 | map = () => [["€".codePointAt(0), 0x80]];
89 | }
90 |
91 | class SJIS extends TwoBytes {
92 | // https://en.wikipedia.org/wiki/Shift_JIS
93 | map = () => SingleByte.generateTable(this.decode.bind(this), 0xa1, 0xdf);
94 | }
95 |
96 | function preferUTF8(
97 | text,
98 | utf8,
99 | encoding = document.characterSet.toLowerCase()
100 | ) {
101 | // Check if text with given encoding is properly encodes;
102 | // The argmuent utf8 is the same data encoded with UTF-8;
103 | // Return true if we should discard given encoding and use UTF-8 encoding instead
104 | if (encoding == "utf-8") return false;
105 | const encoded = new TextDecoder(encoding).decode(
106 | new TextEncoder().encode(utf8)
107 | );
108 | const length = Math.min(text.length, encoded.length);
109 | const result = text.slice(0, length) == encoded.slice(0, length);
110 | const msg = "The declared encoding is " + (result ? "incorrect" : "correct");
111 | console.debug(msg);
112 | return result;
113 | }
114 |
115 | function fixEncoding(tryPart = false, tryFetch = true, node) {
116 | // return false if failed
117 | node = node || document.querySelector("body > pre");
118 | const url = window.location.href;
119 | if (!node) return false;
120 | const text = node.textContent;
121 | const encoding = document.characterSet.toLowerCase();
122 | if (
123 | url.startsWith("file://") ||
124 | document.characterSet == "UTF-8" ||
125 | /^[\p{ASCII}]*$/u.test(text)
126 | )
127 | return true;
128 | if (window.content) {
129 | if (!window.content.fixed) {
130 | const utf8 = window.content["utf-8"];
131 | if (preferUTF8(text, utf8, encoding)) node.textContent = utf8;
132 | }
133 | window.content.fixed = true;
134 | return true;
135 | }
136 | let converter = () => invalidChar;
137 | let encoder = null;
138 | if (
139 | encoding.startsWith("windows") ||
140 | encoding.startsWith("iso-8859") ||
141 | encoding.startsWith("koi") ||
142 | encoding.startsWith("ibm") ||
143 | encoding.includes("mac")
144 | ) {
145 | encoder = new SingleByte(encoding);
146 | } else if (encoding.startsWith("gb")) {
147 | encoder = new GBK(encoding);
148 | } else if (encoding == "shift_jis") {
149 | encoder = new SJIS(encoding);
150 | } else {
151 | encoder = new TwoBytes(encoding);
152 | }
153 | if (encoder !== null) converter = encoder.convert.bind(encoder);
154 | let failed, converted;
155 | if (!tryPart && text.includes(invalidChar)) {
156 | failed = true;
157 | } else {
158 | converted = text.replace(/[^\p{ASCII}]+/gu, converter);
159 | failed = converted.includes(invalidChar);
160 | }
161 | if (!failed || tryPart) node.textContent = converted;
162 | if (tryFetch && failed) {
163 | return new Promise((resolve, _reject) => {
164 | fetch(url, { cache: "force-cache", mode: "same-origin" })
165 | .then((res) => res.text())
166 | .then((utf8) => {
167 | node.textContent = preferUTF8(text, utf8, encoding) ? utf8 : text;
168 | resolve(true);
169 | })
170 | .catch((_e) => resolve(false));
171 | });
172 | } else {
173 | return !failed;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/utils/XMLHttpRequest.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.utils
2 |
3 | import android.os.Handler
4 | import android.util.Base64
5 | import java.io.IOException
6 | import java.net.HttpCookie
7 | import java.net.HttpURLConnection
8 | import java.net.SocketTimeoutException
9 | import java.net.URL
10 | import org.json.JSONArray
11 | import org.json.JSONObject
12 | import org.matrix.chromext.Chrome
13 | import org.matrix.chromext.Listener
14 | import org.matrix.chromext.script.Local
15 |
16 | class XMLHttpRequest(
17 | id: String,
18 | request: JSONObject,
19 | uuid: Double,
20 | currentTab: Any?,
21 | frameId: String?
22 | ) {
23 | val currentTab = currentTab
24 | val frameId = frameId
25 | val request = request
26 | val response = JSONObject(mapOf("id" to id, "uuid" to uuid))
27 |
28 | var connection: HttpURLConnection? = null
29 | var cookies: List
30 |
31 | val anonymous = request.optBoolean("anonymous")
32 | val binary = request.optBoolean("binary")
33 | val buffersize = request.optInt("buffersize", 8)
34 | val cookie = request.optJSONArray("cookie")
35 | val headers = request.optJSONObject("headers")
36 | val method = request.optString("method")
37 | val nocache = request.optBoolean("nocache")
38 | val timeout = request.optInt("timeout")
39 | val responseType = request.optString("responseType")
40 | val url = URL(request.optString("url"))
41 | val uri = url.toURI()
42 |
43 | init {
44 | if (cookie != null && !anonymous) {
45 | for (i in 0 until cookie.length()) {
46 | runCatching {
47 | HttpCookie.parse(cookie!!.getString(i)).forEach { Chrome.cookieStore.add(uri, it) }
48 | }
49 | }
50 | }
51 | cookies = Chrome.cookieStore.get(uri)
52 | }
53 |
54 | fun abort() {
55 | response("abort", JSONObject().put("abort", "Abort on request"))
56 | }
57 |
58 | fun send() {
59 | connection = url.openConnection() as HttpURLConnection
60 | with(connection!!) {
61 | setRequestMethod(method)
62 | setInstanceFollowRedirects(request.optString("redirect") != "manual")
63 | headers?.keys()?.forEach { setRequestProperty(it, headers.optString(it)) }
64 | setUseCaches(!nocache)
65 | setConnectTimeout(timeout)
66 |
67 | if (!anonymous && cookies.size > 0)
68 | setRequestProperty("Cookie", cookies.map { it.toString() }.joinToString("; "))
69 |
70 | if (request.has("user")) {
71 | val user = request.optString("user")
72 | val password = request.optString("password")
73 | val encoded = Base64.encodeToString(("${user}:${password}").toByteArray(), Base64.DEFAULT)
74 | setRequestProperty("Authorization", "Basic " + encoded)
75 | }
76 |
77 | var data = JSONObject()
78 | runCatching {
79 | if (method != "GET" && request.has("data")) {
80 | val input = request.optString("data")
81 | val bytes =
82 | if (binary) {
83 | Base64.decode(input, Base64.DEFAULT)
84 | } else {
85 | input.toByteArray()
86 | }
87 | setFixedLengthStreamingMode(bytes.size)
88 | outputStream.write(bytes)
89 | }
90 |
91 | data.put("status", responseCode)
92 | data.put("statusText", responseMessage)
93 | val headers = headerFields.filter { it.key != null }.mapValues { JSONArray(it.value) }
94 | data.put("headers", JSONObject(headers))
95 | val binary =
96 | responseType !in listOf("", "text", "document", "json") ||
97 | contentEncoding != null ||
98 | (contentType != null &&
99 | contentType.contains("charset") &&
100 | !contentType.contains("utf-8"))
101 | data.put("binary", binary)
102 |
103 | val buffer = ByteArray(buffersize * DEFAULT_BUFFER_SIZE)
104 | while (true) {
105 | var bytes = 0
106 | while (buffer.size > bytes) {
107 | val b = inputStream.read(buffer, bytes, buffer.size - bytes)
108 | if (b == 0 || b == -1) break
109 | bytes += b
110 | }
111 | if (bytes == 0) break
112 | val chunk =
113 | if (binary) {
114 | Base64.encodeToString(buffer, 0, bytes, Base64.DEFAULT)
115 | } else {
116 | String(buffer, 0, bytes)
117 | }
118 | data.put("chunk", chunk)
119 | data.put("bytes", bytes)
120 | response("progress", data, false)
121 | data.remove("headers")
122 | }
123 | response("load", data)
124 | }
125 | .onFailure {
126 | if (it is IOException) {
127 | data.put("type", it::class.java.name)
128 | data.put("message", it.message)
129 | data.put("stack", it.stackTraceToString())
130 | errorStream?.bufferedReader()?.use { it.readText() }?.let { data.put("error", it) }
131 | if (it is SocketTimeoutException) {
132 | response("timeout", data.put("bytesTransferred", it.bytesTransferred))
133 | } else {
134 | response("error", data)
135 | }
136 | }
137 | }
138 | }
139 | if (!anonymous && connection != null) {
140 | Chrome.storeCoookies(this, connection!!.headerFields)
141 | }
142 | }
143 |
144 | fun response(
145 | type: String,
146 | data: JSONObject = JSONObject(),
147 | disconnect: Boolean = true,
148 | ) {
149 | response.put("type", type)
150 | response.put("data", data)
151 | val code = "Symbol.${Local.name}.unlock(${Local.key}).post('xmlhttpRequest', ${response});"
152 | Handler(Chrome.getContext().mainLooper).post {
153 | Chrome.evaluateJavascript(listOf(code), currentTab, frameId)
154 | }
155 | if (disconnect) {
156 | Listener.xmlhttpRequests.remove(response.getDouble("uuid"))
157 | connection?.disconnect()
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/script/Local.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.script
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import java.io.File
6 | import java.io.FileReader
7 | import kotlin.random.Random
8 | import org.json.JSONArray
9 | import org.json.JSONObject
10 | import org.matrix.chromext.Chrome
11 | import org.matrix.chromext.Resource
12 | import org.matrix.chromext.utils.Log
13 | import org.matrix.chromext.utils.randomString
14 |
15 | object GM {
16 | private val localScript: Map
17 |
18 | init {
19 | val ctx = Chrome.getContext()
20 | Resource.enrich(ctx)
21 | localScript =
22 | ctx.assets
23 | .open("GM.js")
24 | .bufferedReader()
25 | .use { it.readText() }
26 | .split("// Kotlin separator\n\n")
27 | .associateBy(
28 | {
29 | val decalre = it.lines()[0]
30 | val sep = if (decalre.startsWith("function")) "(" else " ="
31 | decalre.split(sep)[0].split(" ").last()
32 | },
33 | { it })
34 | }
35 |
36 | fun bootstrap(
37 | script: Script,
38 | codes: MutableList = mutableListOf(),
39 | ): MutableList {
40 | var code = script.code
41 | var grants = ""
42 |
43 | if (!script.meta.startsWith("// ==UserScript==")) {
44 | code = script.meta + code
45 | }
46 |
47 | script.grant.forEach {
48 | when (it) {
49 | "none" -> return@forEach
50 | "frames" -> return@forEach
51 | "GM_info" -> return@forEach
52 | "GM.ChromeXt" -> return@forEach
53 | "window.close" -> return@forEach
54 | else ->
55 | if (localScript.containsKey(it)) {
56 | grants += localScript.get(it)
57 | } else if (it.startsWith("GM_")) {
58 | grants +=
59 | "function ${it}(){ console.error('${it} is not implemented in ChromeXt yet, called with', arguments) }\n"
60 | } else if (it.startsWith("GM.")) {
61 | val func = it.substring(3)
62 | val name =
63 | "GM_" +
64 | if (func == "xmlHttpRequest") {
65 | "xmlhttpRequest"
66 | } else if (func == "getResourceUrl") {
67 | "getResourceURL"
68 | } else {
69 | func
70 | }
71 | if (localScript.containsKey(name) && !script.grant.contains(name))
72 | grants += localScript.get(name)
73 | grants += "${it}={sync: ${name}};\n"
74 | }
75 | }
76 | }
77 |
78 | grants += localScript.get("GM.bootstrap")!!
79 | val GM_info = JSONObject(mapOf("scriptMetaStr" to script.meta))
80 | GM_info.put("script", JSONObject().put("id", script.id))
81 | if (script.storage != null) GM_info.put("storage", script.storage)
82 | code = "\ndelete window.__loading__;\n${code};"
83 | code = localScript.get("globalThis")!! + script.lib.joinToString("\n") + code
84 | codes.add(
85 | "(()=>{ const GM = {key:${Local.key}, name:'${Local.name}'}; const GM_info = ${GM_info}; GM_info.script.code = (key=null) => {${code}};\n${grants}GM.bootstrap();})();\n//# sourceURL=local://ChromeXt/${Uri.encode(script.id)}")
86 | return codes
87 | }
88 | }
89 |
90 | object Local {
91 |
92 | val promptInstallUserScript: String
93 | val customizeDevTool: String
94 | val eruda: String
95 | val encoding: String
96 | val initChromeXt: String
97 | val openEruda: String
98 | val cspRule: String
99 | val cosmeticFilter: String
100 | val key = Random.nextDouble()
101 | val name = randomString(25)
102 |
103 | var eruda_version: String?
104 |
105 | val anchorInChromeXt: Int
106 |
107 | // lineNumber of the anchor in GM.js, used to verify ChromeXt.dispatch
108 |
109 | init {
110 | val ctx = Chrome.getContext()
111 | Resource.enrich(ctx)
112 | var css =
113 | JSONArray(
114 | ctx.assets.open("editor.css").bufferedReader().use { it.readText() }.split("\n\n"))
115 | promptInstallUserScript =
116 | "const _editor_style = ${css}[0];\n" +
117 | ctx.assets.open("editor.js").bufferedReader().use { it.readText() }
118 | customizeDevTool = ctx.assets.open("devtools.js").bufferedReader().use { it.readText() }
119 | css =
120 | JSONArray(ctx.assets.open("eruda.css").bufferedReader().use { it.readText() }.split("\n\n"))
121 | eruda =
122 | "eruda._styles = ${css};\n" +
123 | ctx.assets
124 | .open("eruda.js")
125 | .bufferedReader()
126 | .use { it.readText() }
127 | .replaceFirst("Symbol.ChromeXt", "Symbol." + name)
128 | .replaceFirst("ChromeXtUnlockKeyForEruda", key.toString())
129 | encoding = ctx.assets.open("encoding.js").bufferedReader().use { it.readText() }
130 | eruda_version = getErudaVersion()
131 | val localScript =
132 | ctx.assets
133 | .open("scripts.js")
134 | .bufferedReader()
135 | .use { it.readText() }
136 | .split("// Kotlin separator\n\n")
137 |
138 | val seed = Random.nextDouble()
139 | // Use empty lines to randomize anchorInChromeXt
140 | val parts =
141 | localScript[0]
142 | .replaceFirst("Symbol.ChromeXt", "Symbol." + name)
143 | .replaceFirst("ChromeXtUnlockKeyForInit", key.toString())
144 | .split("\n")
145 | .filter { if (it.length != 0) true else Random.nextDouble() > seed }
146 | anchorInChromeXt = parts.indexOfFirst { it.endsWith("// Kotlin anchor") } + 2
147 | initChromeXt = parts.joinToString("\n")
148 | openEruda =
149 | localScript[1]
150 | .replaceFirst("Symbol.ChromeXt", "Symbol." + name)
151 | .replaceFirst("ChromeXtUnlockKeyForEruda", key.toString())
152 | cspRule = localScript[2]
153 | cosmeticFilter = localScript[3]
154 | }
155 |
156 | fun getErudaVersion(ctx: Context = Chrome.getContext(), versionText: String? = null): String? {
157 | val eruda = File(ctx.filesDir, "Eruda.js")
158 | if (eruda.exists() || versionText != null) {
159 | val verisonReg = Regex(" eruda v(?[\\d\\.]+) https://")
160 | val firstLine = (versionText ?: FileReader(eruda).use { it.readText() }).lines()[0]
161 | val vMatchGroup = verisonReg.find(firstLine)?.groups
162 | if (vMatchGroup != null) {
163 | return vMatchGroup[1]?.value as String
164 | } else if (eruda.exists()) {
165 | eruda.delete()
166 | Log.toast(ctx, "Eruda.js is corrupted")
167 | }
168 | }
169 | return null
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/MainHook.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext
2 |
3 | import android.app.AndroidAppHelper
4 | import android.content.Context
5 | import android.webkit.WebChromeClient
6 | import android.webkit.WebView
7 | import android.webkit.WebViewClient
8 | import de.robv.android.xposed.IXposedHookLoadPackage
9 | import de.robv.android.xposed.IXposedHookZygoteInit
10 | import de.robv.android.xposed.callbacks.XC_LoadPackage
11 | import org.matrix.chromext.hook.BaseHook
12 | import org.matrix.chromext.hook.ContextMenuHook
13 | import org.matrix.chromext.hook.PageInfoHook
14 | import org.matrix.chromext.hook.PageMenuHook
15 | import org.matrix.chromext.hook.PreferenceHook
16 | import org.matrix.chromext.hook.UserScriptHook
17 | import org.matrix.chromext.hook.WebViewHook
18 | import org.matrix.chromext.utils.Log
19 | import org.matrix.chromext.utils.findMethodOrNull
20 | import org.matrix.chromext.utils.hookAfter
21 |
22 | val supportedPackages =
23 | arrayOf(
24 | "app.vanadium.browser",
25 | "com.android.chrome",
26 | "com.brave.browser",
27 | "com.brave.browser_beta",
28 | "com.brave.browser_nightly",
29 | "com.chrome.beta",
30 | "com.chrome.canary",
31 | "com.chrome.dev",
32 | "com.coccoc.trinhduyet",
33 | "com.coccoc.trinhduyet_beta",
34 | "com.herond.android.browser",
35 | "com.kiwibrowser.browser",
36 | "com.microsoft.emmx",
37 | "com.microsoft.emmx.beta",
38 | "com.microsoft.emmx.canary",
39 | "com.microsoft.emmx.dev",
40 | "com.naver.whale",
41 | "com.sec.android.app.sbrowser",
42 | "com.sec.android.app.sbrowser.beta",
43 | "com.vivaldi.browser",
44 | "com.vivaldi.browser.snapshot",
45 | "org.axpos.aosmium",
46 | "org.bromite.bromite",
47 | "org.chromium.chrome",
48 | "org.chromium.thorium",
49 | "org.cromite.cromite",
50 | "org.greatfire.freebrowser",
51 | "org.triple.banana",
52 | "us.spotco.mulch")
53 |
54 | class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
55 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
56 | Log.d(lpparam.processName + " started")
57 | if (lpparam.packageName == "org.matrix.chromext") return
58 | if (supportedPackages.contains(lpparam.packageName)) {
59 | lpparam.classLoader
60 | .loadClass("org.chromium.ui.base.WindowAndroid")
61 | .declaredConstructors[1]
62 | .hookAfter {
63 | Chrome.init(it.args[0] as Context, lpparam.packageName)
64 | initHooks(UserScriptHook)
65 | if (ContextMenuHook.isInit) return@hookAfter
66 | runCatching {
67 | if (!Chrome.isVivaldi) initHooks(PreferenceHook)
68 | initHooks(if (Chrome.isEdge || Chrome.isCocCoc) PageInfoHook else PageMenuHook)
69 | }
70 | .onFailure {
71 | initHooks(ContextMenuHook)
72 | if (BuildConfig.DEBUG && !(Chrome.isSamsung || Chrome.isEdge)) Log.ex(it)
73 | }
74 | }
75 | } else {
76 | val ctx = AndroidAppHelper.currentApplication()
77 |
78 | Chrome.isMi =
79 | Chrome.isMi ||
80 | lpparam.packageName == "com.mi.globalbrowser" ||
81 | lpparam.packageName == "com.android.browser"
82 | Chrome.isQihoo = lpparam.packageName == "com.qihoo.contents"
83 |
84 | if (ctx == null && Chrome.isMi) return
85 | // Wait to get the browser context of Mi Browser
86 |
87 | if (ctx != null && lpparam.packageName != "android") Chrome.init(ctx, ctx.packageName)
88 |
89 | if (Chrome.isMi) {
90 | WebViewHook.WebView = Chrome.load("com.miui.webkit.WebView")
91 | runCatching {
92 | WebViewHook.ViewClient = Chrome.load("com.android.browser.tab.TabWebViewClient")
93 | WebViewHook.ChromeClient = Chrome.load("com.android.browser.tab.TabWebChromeClient")
94 | }
95 | .onFailure {
96 | val miuiAutologinBar = Chrome.load("com.android.browser.MiuiAutologinBar")
97 | // Use MiuiAutologinBar to find `com.android.browser.tab.Tab`, which can located by
98 | // searching the string "X-MiOrigin"
99 | val fields = miuiAutologinBar.declaredFields.map { it.type }
100 | val tab =
101 | miuiAutologinBar.declaredMethods
102 | .find {
103 | it.parameterCount == 2 &&
104 | it.parameterTypes[1] == Boolean::class.java &&
105 | !fields.contains(it.parameterTypes[0])
106 | }!!
107 | .run { parameterTypes[0] }
108 | tab.declaredFields.forEach {
109 | if (findMethodOrNull(it.type) {
110 | // Found by searching the string "Console: "
111 | it.name == "onGeolocationPermissionsHidePrompt"
112 | } != null)
113 | WebViewHook.ChromeClient = it.type
114 | if (findMethodOrNull(it.type) {
115 | // Found by searching the string "Tab.MainWebViewClient"
116 | it.name == "onReceivedHttpAuthRequest"
117 | } != null)
118 | WebViewHook.ViewClient = it.type
119 | }
120 | }
121 |
122 | hookWebView()
123 | return
124 | }
125 |
126 | if (Chrome.isQihoo) {
127 | WebViewHook.WebView = Chrome.load("com.qihoo.webkit.WebView")
128 | WebViewHook.ViewClient = Chrome.load("com.qihoo.webkit.WebViewClient")
129 | WebViewHook.ChromeClient = Chrome.load("com.qihoo.webkit.WebChromeClient")
130 | hookWebView()
131 | return
132 | }
133 |
134 | WebViewClient::class.java.declaredConstructors[0].hookAfter {
135 | if (it.thisObject::class != WebViewClient::class) {
136 | WebViewHook.ViewClient = it.thisObject::class.java
137 | hookWebView()
138 | }
139 | }
140 |
141 | WebChromeClient::class.java.declaredConstructors[0].hookAfter {
142 | if (it.thisObject::class != WebChromeClient::class) {
143 | WebViewHook.ChromeClient = it.thisObject::class.java
144 | hookWebView()
145 | }
146 | }
147 | }
148 | }
149 |
150 | private fun hookWebView() {
151 | if (WebViewHook.ChromeClient == null || WebViewHook.ViewClient == null) return
152 | if (WebViewHook.WebView == null) {
153 | runCatching {
154 | WebViewHook.WebView = WebView::class.java
155 | WebView.setWebContentsDebuggingEnabled(true)
156 | }
157 | .onFailure { if (BuildConfig.DEBUG) Log.ex(it) }
158 | }
159 | initHooks(WebViewHook, ContextMenuHook)
160 | }
161 |
162 | override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
163 | Resource.init(startupParam.modulePath)
164 | }
165 |
166 | private fun initHooks(vararg hook: BaseHook) {
167 | hook.forEach {
168 | if (it.isInit) return@forEach
169 | it.init()
170 | if (it.isInit) Log.d("${it.javaClass.simpleName} hooked")
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/proxy/UserScript.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.proxy
2 |
3 | import android.net.Uri
4 | import android.view.ContextThemeWrapper
5 | import java.lang.reflect.Modifier
6 | import org.matrix.chromext.Chrome
7 | import org.matrix.chromext.script.ScriptDbManager
8 | import org.matrix.chromext.utils.Log
9 | import org.matrix.chromext.utils.findField
10 | import org.matrix.chromext.utils.findMethod
11 | import org.matrix.chromext.utils.findMethodOrNull
12 | import org.matrix.chromext.utils.invokeMethod
13 | import org.matrix.chromext.utils.parseOrigin
14 |
15 | object UserScriptProxy {
16 | // It is possible to do a HTTP POST with LoadUrlParams Class
17 | // grep org/chromium/content_public/common/ResourceRequestBody to get setPostData in
18 | // org/chromium/content_public/browser/LoadUrlParams
19 |
20 | val gURL = Chrome.load("org.chromium.url.GURL")
21 | val loadUrlParams =
22 | if (Chrome.isSamsung) {
23 | Chrome.load("com.sec.android.app.sbrowser.tab.LoadUrlParams")
24 | } else {
25 | Chrome.load("org.chromium.content_public.browser.LoadUrlParams")
26 | }
27 | // val tabModelJniBridge = Chrome.load("org.chromium.chrome.browser.tabmodel.TabModelJniBridge")
28 | val tabWebContentsDelegateAndroidImpl =
29 | if (Chrome.isSamsung) {
30 | Chrome.load("com.sec.android.app.sbrowser.tab.Tab")
31 | } else {
32 | Chrome.load("org.chromium.chrome.browser.tab.TabWebContentsDelegateAndroidImpl")
33 | }
34 | val navigationControllerImpl =
35 | Chrome.load("org.chromium.content.browser.framehost.NavigationControllerImpl")
36 | val chromeTabbedActivity =
37 | if (Chrome.isSamsung) {
38 | Chrome.load("com.sec.terrace.TerraceActivity")
39 | } else {
40 | Chrome.load("org.chromium.chrome.browser.ChromeTabbedActivity")
41 | }
42 | val tabImpl =
43 | if (Chrome.isSamsung) {
44 | Chrome.load("com.sec.terrace.Terrace")
45 | } else {
46 | Chrome.load("org.chromium.chrome.browser.tab.TabImpl")
47 | }
48 | private val getId = findMethodOrNull(tabImpl) { name == "getId" }
49 | private val mId =
50 | (if (Chrome.isSamsung) tabWebContentsDelegateAndroidImpl else tabImpl)
51 | .declaredFields
52 | .run {
53 | val target = find { it.name == "mId" }
54 | if (target == null) {
55 | val profile = Chrome.load("org.chromium.chrome.browser.profiles.Profile")
56 | val windowAndroid = Chrome.load("org.chromium.ui.base.WindowAndroid")
57 | var startIndex = indexOfFirst { it.type == gURL }
58 | val endIndex = indexOfFirst {
59 | it.type == profile ||
60 | it.type == ContextThemeWrapper::class.java ||
61 | it.type == windowAndroid
62 | }
63 | if (startIndex == -1 || startIndex > endIndex) startIndex = 0
64 | slice(startIndex..endIndex - 1).findLast { it.type == Int::class.java }!!
65 | } else target
66 | }
67 | .also { it.isAccessible = true }
68 | val mTab = findField(tabWebContentsDelegateAndroidImpl) { type == tabImpl }
69 | val mIsLoading =
70 | tabImpl.declaredFields
71 | .run {
72 | // mIsLoading is used in method stopLoading, before calling
73 | // Lorg/chromium/content_public/browser/WebContents;->stop()V
74 | val target = find { it.name == "mIsLoading" }
75 | if (target == null) {
76 | val webContents = Chrome.load("org.chromium.content_public.browser.WebContents")
77 | val startIndex =
78 | maxOf(
79 | indexOfFirst { it.type == webContents },
80 | indexOfFirst { it.type == loadUrlParams })
81 | slice(startIndex..size - 1).find {
82 | it.type == Boolean::class.java && !Modifier.isStatic(it.modifiers)
83 | }!!
84 | } else target
85 | }
86 | .also { it.isAccessible = true }
87 | val loadUrl =
88 | findMethod(if (Chrome.isSamsung) tabWebContentsDelegateAndroidImpl else tabImpl) {
89 | parameterTypes contentDeepEquals arrayOf(loadUrlParams) &&
90 | (Chrome.isSamsung || returnType != Void.TYPE)
91 | }
92 |
93 | val kMaxURLChars = 2097152
94 |
95 | private fun loadUrl(url: String, tab: Any? = Chrome.getTab()) {
96 | if (!Chrome.isSamsung && !Chrome.checkTab(tab)) return
97 | loadUrl.invoke(tab, newLoadUrlParams(url))
98 | }
99 |
100 | fun getTabId(tab: Any): String {
101 | val id = if (getId != null) getId.invoke(tab)!! else mId.get(tab)!!
102 | return id.toString()
103 | }
104 |
105 | fun newLoadUrlParams(url: String): Any {
106 | val constructor =
107 | loadUrlParams.declaredConstructors.find { it.parameterTypes.contains(String::class.java) }!!
108 | val types = constructor.parameterTypes
109 | if (types contentDeepEquals arrayOf(Int::class.java, String::class.java)) {
110 | return constructor.newInstance(0, url)
111 | } else if (types contentDeepEquals arrayOf(String::class.java, Int::class.java)) {
112 | return constructor.newInstance(url, 0)
113 | } else {
114 | return constructor.newInstance(url)
115 | }
116 | }
117 |
118 | fun evaluateJavascript(script: String, tab: Any? = Chrome.getTab()): Boolean {
119 | if (script == "") return true
120 | if (Chrome.isSamsung) {
121 | mTab.get(tab ?: Chrome.getTab())?.invokeMethod(script, null) {
122 | name == "evaluateJavaScriptForTests"
123 | }
124 | return true
125 | }
126 | if (script.length > kMaxURLChars - 20000) return false
127 | val code = Uri.encode(script)
128 | if (code.length < kMaxURLChars - 200) {
129 | loadUrl("javascript:${code}", tab ?: Chrome.getTab())
130 | return true
131 | } else {
132 | return false
133 | }
134 | }
135 |
136 | fun getTab(delegate: Any): Any? {
137 | return if (Chrome.isSamsung) delegate else mTab.get(delegate)
138 | }
139 |
140 | fun parseUrl(packed: Any?): String? {
141 | if (packed == null) {
142 | return null
143 | } else if (packed::class.java == String::class.java) {
144 | return packed as String
145 | } else if (packed::class.java == loadUrlParams) {
146 | val mUrl = loadUrlParams.getDeclaredField("a")
147 | return mUrl.get(packed) as String
148 | } else if (packed::class.java == gURL) {
149 | val mSpec = gURL.getDeclaredField("a")
150 | return mSpec.get(packed) as String
151 | }
152 | Log.e("parseUrl: ${packed::class.java} is not ${loadUrlParams.name} nor ${gURL.name}")
153 | return null
154 | }
155 |
156 | fun userAgentHook(url: String, urlParams: Any): Boolean {
157 | val origin = parseOrigin(url)
158 | if (origin != null) {
159 | // Log.d("Change User-Agent header: ${origin}")
160 | if (ScriptDbManager.userAgents.contains(origin)) {
161 | val header = "user-agent: ${ScriptDbManager.userAgents.get(origin)}\r\n"
162 | if (Chrome.isSamsung) {
163 | urlParams.invokeMethod(header) { name == "setVerbatimHeaders" }
164 | } else {
165 | val mVerbatimHeaders =
166 | loadUrlParams.declaredFields.filter { it.type == String::class.java }[1]
167 | mVerbatimHeaders.set(urlParams, header)
168 | }
169 | return true
170 | }
171 | }
172 | return false
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/hook/UserScript.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.hook
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.ServiceConnection
6 | import android.net.http.HttpResponseCache
7 | import org.matrix.chromext.BuildConfig
8 | import org.matrix.chromext.Chrome
9 | import org.matrix.chromext.Listener
10 | import org.matrix.chromext.proxy.UserScriptProxy
11 | import org.matrix.chromext.script.Local
12 | import org.matrix.chromext.script.ScriptDbManager
13 | import org.matrix.chromext.utils.Log
14 | import org.matrix.chromext.utils.findField
15 | import org.matrix.chromext.utils.findFieldOrNull
16 | import org.matrix.chromext.utils.findMethod
17 | import org.matrix.chromext.utils.findMethodOrNull
18 | import org.matrix.chromext.utils.hookAfter
19 | import org.matrix.chromext.utils.hookBefore
20 |
21 | object UserScriptHook : BaseHook() {
22 |
23 | override fun init() {
24 |
25 | val proxy = UserScriptProxy
26 |
27 | // proxy.tabModelJniBridge.declaredConstructors[0].hookAfter {
28 | // Chrome.addTabModel(it.thisObject)
29 | // }
30 |
31 | // findMethod(proxy.tabModelJniBridge) { name == "destroy" }
32 | // .hookBefore { Chrome.dropTabModel(it.thisObject) }
33 |
34 | if (Chrome.isSamsung) {
35 | findMethodOrNull(proxy.tabWebContentsDelegateAndroidImpl) { name == "onDidFinishNavigation" }
36 | .let {
37 | if (it == null)
38 | findMethod(proxy.tabWebContentsDelegateAndroidImpl) {
39 | name == "updateBrowserControlsState"
40 | }
41 | else it
42 | }
43 | .hookAfter { Chrome.updateTab(it.thisObject) }
44 |
45 | runCatching {
46 | // Avoid exceptions thrown due to signature conficts while binding services
47 | val ConnectionManager =
48 | Chrome.load("com.samsung.android.sdk.scs.base.connection.ConnectionManager")
49 | val mServiceConnection =
50 | findField(ConnectionManager) { name == "mServiceConnection" }
51 | .also { it.isAccessible = true }
52 |
53 | findMethod(ConnectionManager) { name == "connectToService" }
54 | // (Landroid/content/Context;Landroid/content/Intent;)Z
55 | .hookBefore {
56 | val hook = it
57 | val ctx = hook.args[0] as Context
58 | val intent = hook.args[1] as Intent
59 | val connection = mServiceConnection.get(hook.thisObject) as ServiceConnection
60 | runCatching {
61 | if (BuildConfig.DEBUG) Log.d("Binding service ${intent} with ${ctx}")
62 | val bound = ctx.bindService(intent, connection, Context.BIND_AUTO_CREATE)
63 | hook.result = bound
64 | }
65 | .onFailure {
66 | if (BuildConfig.DEBUG) Log.ex(it)
67 | hook.result = false
68 | }
69 | }
70 | }
71 | .onFailure { if (BuildConfig.DEBUG) Log.ex(it) }
72 |
73 | runCatching {
74 | // Avoid version codes mismatch when isolated child services are connected
75 | val childProcessConnection =
76 | Chrome.load("org.chromium.base.process_launcher.ChildProcessConnection")
77 | val packageUtils = Chrome.load("org.chromium.base.PackageUtils")
78 | val buildInfo = Chrome.load("org.chromium.base.BuildInfo")
79 | val buildConifg = Chrome.load("org.chromium.build.BuildConfig")
80 |
81 | val mServiceName = findField(childProcessConnection) { name == "mServiceName" }
82 | val getApplicationPackageInfo =
83 | findMethod(packageUtils) { name == "getApplicationPackageInfo" }
84 | val packageVersionCode = findMethod(buildInfo) { name == "packageVersionCode" }
85 | val version_code = findFieldOrNull(buildConifg) { name == "VERSION_CODE" }
86 |
87 | if (version_code != null) {
88 | findMethod(childProcessConnection) { name == "onServiceConnectedOnLauncherThread" }
89 | // (Landroid/os/IBinder;)V
90 | .hookBefore {
91 | val latestPackage = getApplicationPackageInfo.invoke(null, 0)
92 | val latestVersionCode = packageVersionCode.invoke(null, latestPackage)
93 | val loadedVersionCode = version_code.get(null)
94 | if (latestVersionCode != loadedVersionCode) {
95 | Log.d(
96 | "Version codes mismatched for child services ${mServiceName.get(it.thisObject)}")
97 | version_code.set(null, latestVersionCode)
98 | }
99 | }
100 | }
101 | }
102 | .onFailure { if (BuildConfig.DEBUG) Log.ex(it) }
103 | }
104 |
105 | findMethod(if (Chrome.isSamsung) proxy.tabImpl else proxy.tabWebContentsDelegateAndroidImpl) {
106 | name == "onUpdateUrl"
107 | }
108 | // public void onUpdateUrl(GURL url)
109 | .hookAfter {
110 | val tab = proxy.getTab(it.thisObject)!!
111 | if (!Chrome.isSamsung) Chrome.updateTab(tab)
112 | val url = proxy.parseUrl(it.args[0])!!
113 | val isLoading = proxy.mIsLoading.get(tab) as Boolean
114 | if (!url.startsWith("chrome") && isLoading) {
115 | ScriptDbManager.invokeScript(url)
116 | }
117 | }
118 |
119 | findMethod(proxy.tabWebContentsDelegateAndroidImpl) {
120 | name == if (Chrome.isSamsung) "onAddMessageToConsole" else "addMessageToConsole"
121 | }
122 | // public boolean addMessageToConsole(int level, String message, int lineNumber,
123 | // String sourceId)
124 | .hookAfter {
125 | // This should be the way to communicate with the front-end of ChromeXt
126 | val lineNumber = it.args[2] as Int
127 | val sourceId = it.args[3] as String
128 | if (it.args[0] as Int == 0 &&
129 | sourceId.startsWith("local://ChromeXt/init") &&
130 | lineNumber == Local.anchorInChromeXt) {
131 | Listener.startAction(it.args[1] as String, proxy.getTab(it.thisObject), null, sourceId)
132 | } else {
133 | Log.d(
134 | when (it.args[0] as Int) {
135 | 0 -> "D"
136 | 2 -> "W"
137 | 3 -> "E"
138 | else -> "V"
139 | } + ": [${sourceId}@${lineNumber}] ${it.args[1]}")
140 | }
141 | }
142 |
143 | findMethod(proxy.navigationControllerImpl) {
144 | name == "loadUrl" || parameterTypes contentDeepEquals arrayOf(proxy.loadUrlParams)
145 | }
146 | // public void loadUrl(LoadUrlParams params)
147 | .hookBefore {
148 | val url = proxy.parseUrl(it.args[0])!!
149 | proxy.userAgentHook(url, it.args[0])
150 | }
151 |
152 | findMethod(proxy.chromeTabbedActivity, true) { name == "onResume" }
153 | .hookBefore { Chrome.init(it.thisObject as Context) }
154 |
155 | findMethod(proxy.chromeTabbedActivity) { name == "onStop" }
156 | .hookBefore {
157 | ScriptDbManager.updateScriptStorage()
158 | val cache = HttpResponseCache.getInstalled()
159 | Log.d("HttpResponseCache: Hit ${cache.hitCount} / NetWork ${cache.networkCount}")
160 | cache.flush()
161 | }
162 | isInit = true
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/devtools/WebSocketClient.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.devtools
2 |
3 | import android.net.LocalSocket
4 | import android.net.LocalSocketAddress
5 | import android.os.Process
6 | import android.util.Base64
7 | import java.io.OutputStream
8 | import java.security.SecureRandom
9 | import kotlin.experimental.xor
10 | import org.json.JSONObject
11 | import org.matrix.chromext.Chrome
12 | import org.matrix.chromext.hook.UserScriptHook
13 | import org.matrix.chromext.hook.WebViewHook
14 | import org.matrix.chromext.utils.Log
15 | import org.matrix.chromext.utils.randomString
16 |
17 | class DevToolClient(tabId: String, tag: String? = null) : LocalSocket() {
18 |
19 | val tabId = tabId
20 | val tag = tag
21 | private var cspBypassed = false
22 | private var id = 1
23 | private var listening = false
24 | private var mClosed = false
25 | private var pageEnabled = false
26 |
27 | init {
28 | connectDevTools(this)
29 | val request =
30 | arrayOf(
31 | "GET /devtools/page/${tabId} HTTP/1.1",
32 | "Connection: Upgrade",
33 | "Upgrade: websocket",
34 | "Sec-WebSocket-Version: 13",
35 | "Sec-WebSocket-Key: ${Base64.encodeToString(randomString(16).toByteArray(), Base64.DEFAULT).trim()}")
36 | Log.d("Start inspecting tab ${tabId}")
37 | outputStream.write((request.joinToString("\r\n") + "\r\n\r\n").toByteArray())
38 | val buffer = ByteArray(DEFAULT_BUFFER_SIZE / 8)
39 | inputStream.read(buffer)
40 | if (String(buffer).split("\r\n")[0] != "HTTP/1.1 101 WebSocket Protocol Handshake") {
41 | close()
42 | Log.d("Fail to inspect tab ${tabId} with response\n" + String(buffer), true)
43 | }
44 | }
45 |
46 | override fun close() {
47 | super.close()
48 | mClosed = true
49 | }
50 |
51 | override fun isClosed(): Boolean {
52 | return mClosed
53 | }
54 |
55 | fun isPageEnabled(forceActivate: Boolean = false): Boolean {
56 | val status = pageEnabled
57 | if (forceActivate && !pageEnabled) {
58 | command(null, "Page.enable", JSONObject())
59 | pageEnabled = true
60 | }
61 | return status
62 | }
63 |
64 | fun command(id: Int?, method: String, params: JSONObject?) {
65 | if (isClosed()) {
66 | return
67 | }
68 | val msg = JSONObject(mapOf("method" to method))
69 | if (params != null) msg.put("params", params)
70 |
71 | if (id == null) {
72 | msg.put("id", this.id)
73 | this.id += 1
74 | } else {
75 | msg.put("id", id)
76 | }
77 |
78 | WebSocketFrame(msg.toString()).write(outputStream)
79 | }
80 |
81 | fun bypassCSP(bypass: Boolean) {
82 | if (cspBypassed == bypass) return
83 | isPageEnabled(true)
84 | command(null, "Page.setBypassCSP", JSONObject().put("enabled", bypass))
85 | cspBypassed = bypass
86 | if (bypass) DevSessions.add(this)
87 | }
88 |
89 | fun evaluateJavascript(script: String) {
90 | command(this.id, "Runtime.evaluate", JSONObject(mapOf("expression" to script)))
91 | this.id += 1
92 | }
93 |
94 | fun ping(msg: String = "heartbeat") {
95 | WebSocketFrame(msg, 0x9).write(outputStream)
96 | }
97 |
98 | fun listen(callback: (JSONObject) -> Unit = { msg -> Log.d(msg.toString()) }) {
99 | if (listening) Log.w("client was being listened on")
100 | listening = true
101 | runCatching {
102 | while (!isClosed()) {
103 | val type = inputStream.read()
104 | if (type == -1) {
105 | break
106 | } else if (type == (0x80 or 0x1) || type == (0x80 or 0xA)) {
107 | var len = inputStream.read()
108 | if (len == 0x7e) {
109 | len = inputStream.read() shl 8
110 | len += inputStream.read()
111 | } else if (len == 0x7f) {
112 | len = 0
113 | for (i in 0 until 8) {
114 | len = len or (inputStream.read() shl (8 * (7 - i)))
115 | }
116 | } else if (len > 0x7d) {
117 | throw Exception("Invalid frame length ${len}")
118 | }
119 | var offset = 0
120 | val buffer = ByteArray(len)
121 | while (offset != len) offset += inputStream.read(buffer, offset, len - offset)
122 | val frame = String(buffer)
123 |
124 | if (type == (0x80 or 0xA)) {
125 | callback(JSONObject(mapOf("pong" to frame)))
126 | } else {
127 | callback(JSONObject(frame))
128 | }
129 | } else {
130 | throw Exception("Invalid frame type ${type}")
131 | }
132 | }
133 | }
134 | .onFailure {
135 | if (!isClosed()) {
136 | Log.ex(it, "Listening at tab ${tabId}")
137 | }
138 | }
139 | close()
140 | }
141 | }
142 |
143 | class WebSocketFrame(msg: String?, opcode: Int = 0x1) {
144 | private val mFin: Int
145 | private val mRsv1: Int
146 | private val mRsv2: Int
147 | private val mRsv3: Int
148 | private val mOpcode: Int
149 | private val mPayload: ByteArray
150 |
151 | var mMask: Boolean = false
152 |
153 | init {
154 | mFin = 0x80
155 | mRsv1 = 0x00
156 | mRsv2 = 0x00
157 | mRsv3 = 0x00
158 | mOpcode = opcode
159 | mPayload =
160 | if (msg == null) {
161 | ByteArray(0)
162 | } else {
163 | msg.toByteArray()
164 | }
165 | }
166 |
167 | fun write(os: OutputStream) {
168 | writeFrame0(os)
169 | writeFrame1(os)
170 | writeFrameExtendedPayloadLength(os)
171 | val maskingKey = ByteArray(4)
172 | SecureRandom().nextBytes(maskingKey)
173 | os.write(maskingKey)
174 | writeFramePayload(os, maskingKey)
175 | }
176 |
177 | private fun writeFrame0(os: OutputStream) {
178 | val b = mFin or mRsv1 or mRsv2 or mRsv1 or (mOpcode and 0x0F)
179 | os.write(b)
180 | }
181 |
182 | private fun writeFrame1(os: OutputStream) {
183 | var b = 0x80
184 | val len = mPayload.size
185 | if (len <= 0x7d) {
186 | b = b or len
187 | } else if (len <= 0xffff) {
188 | b = b or 0x7e
189 | } else {
190 | b = b or 0x7f
191 | }
192 | os.write(b)
193 | }
194 |
195 | private fun writeFrameExtendedPayloadLength(os: OutputStream) {
196 | var len = mPayload.size
197 | val buf: ByteArray
198 | if (len <= 0x7d) {
199 | return
200 | } else if (len <= 0xffff) {
201 | buf = ByteArray(2)
202 | buf[1] = (len and 0xff).toByte()
203 | buf[0] = ((len shr 8) and 0xff).toByte()
204 | } else {
205 | buf = ByteArray(8)
206 | for (i in 0 until 8) {
207 | buf[7 - i] = (len and 0xff).toByte()
208 | len = len shr 8
209 | }
210 | }
211 | os.write(buf)
212 | }
213 |
214 | private fun writeFramePayload(os: OutputStream, mask: ByteArray) {
215 | os.write(mPayload.mapIndexed { index, byte -> byte xor mask[index.rem(4)] }.toByteArray())
216 | }
217 | }
218 |
219 | fun connectDevTools(client: LocalSocket) {
220 | val address =
221 | if (UserScriptHook.isInit) {
222 | if (Chrome.isSamsung) {
223 | "Terrace_devtools_remote"
224 | } else {
225 | "chrome_devtools_remote"
226 | }
227 | } else if (Chrome.isMi) {
228 | "miui_webview_devtools_remote"
229 | } else if (WebViewHook.isInit) {
230 | "webview_devtools_remote"
231 | } else {
232 | throw Exception("DevTools started unexpectedly")
233 | }
234 |
235 | runCatching { client.connect(LocalSocketAddress(address)) }
236 | .onFailure { client.connect(LocalSocketAddress(address + "_" + Process.myPid())) }
237 | }
238 |
--------------------------------------------------------------------------------
/app/src/main/java/org/matrix/chromext/script/Manager.kt:
--------------------------------------------------------------------------------
1 | package org.matrix.chromext.script
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.database.AbstractWindowedCursor
6 | import android.database.CursorWindow
7 | import android.net.Uri
8 | import android.os.Build
9 | import org.json.JSONArray
10 | import org.json.JSONObject
11 | import org.matrix.chromext.Chrome
12 | import org.matrix.chromext.utils.Log
13 | import org.matrix.chromext.utils.invokeMethod
14 | import org.matrix.chromext.utils.isChromeXtFrontEnd
15 | import org.matrix.chromext.utils.isDevToolsFrontEnd
16 | import org.matrix.chromext.utils.isUserScript
17 | import org.matrix.chromext.utils.matching
18 | import org.matrix.chromext.utils.parseOrigin
19 | import org.matrix.chromext.utils.resolveContentUrl
20 | import org.matrix.chromext.utils.shouldBypassSandbox
21 |
22 | object ScriptDbManager {
23 |
24 | val scripts = query()
25 | val cosmeticFilters: MutableMap
26 | val userAgents: MutableMap
27 | val cspRules: MutableMap
28 | var keepStorage: Boolean
29 |
30 | init {
31 | val ctx = Chrome.getContext()
32 | @Suppress("UNCHECKED_CAST")
33 | cosmeticFilters =
34 | ctx.getSharedPreferences("CosmeticFilter", Context.MODE_PRIVATE).getAll()
35 | as MutableMap
36 | @Suppress("UNCHECKED_CAST")
37 | userAgents =
38 | ctx.getSharedPreferences("UserAgent", Context.MODE_PRIVATE).getAll()
39 | as MutableMap
40 | @Suppress("UNCHECKED_CAST")
41 | cspRules =
42 | ctx.getSharedPreferences("CSPRule", Context.MODE_PRIVATE).getAll()
43 | as MutableMap
44 |
45 | keepStorage =
46 | ctx.getSharedPreferences("ChromeXt", Context.MODE_PRIVATE).getBoolean("keep_storage", true)
47 | }
48 |
49 | fun insert(vararg script: Script) {
50 | val dbHelper = ScriptDbHelper(Chrome.getContext())
51 | val db = dbHelper.writableDatabase
52 | script.forEach {
53 | val lines = db.delete("script", "id = ?", arrayOf(it.id))
54 | if (lines > 0) {
55 | val id = it.id
56 | Log.d("Update ${lines} rows with id ${id}")
57 | if (keepStorage) it.storage = scripts.find { it.id == id }?.storage
58 | }
59 | val values =
60 | ContentValues().apply {
61 | put("id", it.id)
62 | put("code", it.code)
63 | put("meta", it.meta)
64 | if (it.storage != null) {
65 | put("storage", it.storage.toString())
66 | }
67 | }
68 | runCatching { db.insertOrThrow("script", null, values) }
69 | .onFailure {
70 | Log.e("Fail to store script ${values.getAsString("id")} into SQL database.")
71 | Log.ex(it)
72 | }
73 | }
74 | dbHelper.close()
75 | }
76 |
77 | private fun fixEncoding(url: String, path: String, codes: MutableList) {
78 | if (path.endsWith(".js") || path.endsWith(".txt")) {
79 | // Fix encoding for local text files
80 | val inputStream = Chrome.getContext().contentResolver.openInputStream(Uri.parse(url))
81 | val text = inputStream?.bufferedReader()?.readText()
82 | if (text != null) {
83 | val data = JSONObject(mapOf("utf-8" to text))
84 | codes.add("window.content=${data};")
85 | codes.add(Local.encoding)
86 | }
87 | inputStream?.close()
88 | } else if (url.endsWith(".js") || url.endsWith(".txt")) {
89 | // Fix encoding for remote text files
90 | codes.add(Local.encoding)
91 | }
92 |
93 | if (codes.size > 1 && (url.endsWith(".txt") || path.endsWith(".txt")))
94 | codes.add("fixEncoding();")
95 | }
96 |
97 | fun invokeScript(url: String, webView: Any? = null, frameId: String? = null) {
98 | val codes = mutableListOf(Local.initChromeXt)
99 | val path = resolveContentUrl(url)
100 | val webSettings = webView?.invokeMethod { name == "getSettings" }
101 |
102 | var trustedPage = true
103 | // Whether ChromeXt is accessible in the global context
104 | var runScripts = false
105 | // Whether UserScripts are invoked
106 | var bypassSandbox = false
107 |
108 | fixEncoding(url, path, codes)
109 |
110 | if (isUserScript(url, path)) {
111 | trustedPage = false
112 | codes.add(Local.promptInstallUserScript)
113 | bypassSandbox = shouldBypassSandbox(url)
114 | } else if (isDevToolsFrontEnd(url)) {
115 | codes.add(Local.customizeDevTool)
116 | webSettings?.invokeMethod(null) { name == "setUserAgentString" }
117 | } else if (!isChromeXtFrontEnd(url)) {
118 | val origin = parseOrigin(url)
119 | if (origin != null) {
120 | if (cspRules.contains(origin)) {
121 | runCatching {
122 | val rule = JSONArray(cspRules.get(origin))
123 | codes.add("Symbol.ChromeXt.cspRules.push(...${rule});${Local.cspRule}")
124 | }
125 | }
126 | if (cosmeticFilters.contains(origin)) {
127 | runCatching {
128 | val filter = JSONArray(cosmeticFilters.get(origin))
129 | codes.add("Symbol.ChromeXt.filters.push(...${filter});${Local.cosmeticFilter}")
130 | }
131 | }
132 | if (userAgents.contains(origin)) {
133 | val agent = userAgents.get(origin)
134 | codes.add("Object.defineProperties(window.navigator,{userAgent:{value:'${agent}'}});")
135 | webSettings?.invokeMethod(agent) { name == "setUserAgentString" }
136 | }
137 | trustedPage = false
138 | runScripts = true
139 | }
140 | }
141 |
142 | if (trustedPage) {
143 | codes.add("globalThis.ChromeXt = Symbol.ChromeXt;")
144 | } else if (runScripts) {
145 | codes.add("Symbol.ChromeXt.lock(${Local.key}, '${Local.name}');")
146 | }
147 | codes.add("//# sourceURL=local://ChromeXt/init" + if (frameId == null) "" else "/" + frameId)
148 | webSettings?.invokeMethod(true) { name == "setJavaScriptEnabled" }
149 | val initScript = codes.joinToString("\n")
150 |
151 | val asyncEvaluation = bypassSandbox || frameId != null
152 | var framesGranted = false
153 |
154 | if (!asyncEvaluation)
155 | Chrome.evaluateJavascript(
156 | listOf(initScript), webView, frameId, bypassSandbox, bypassSandbox)
157 | codes.clear()
158 | if (asyncEvaluation) codes.add(initScript)
159 | if (runScripts) {
160 | scripts
161 | .filter { matching(it, url) && !(frameId != null && it.noframes) }
162 | .forEach {
163 | if (it.grant.contains("frames")) framesGranted = true
164 | GM.bootstrap(it, codes)
165 | }
166 | if (!asyncEvaluation) Chrome.evaluateJavascript(codes, webView, frameId)
167 | }
168 | if (asyncEvaluation)
169 | Chrome.evaluateJavascript(codes, webView, frameId, bypassSandbox, bypassSandbox)
170 |
171 | if (framesGranted && frameId == null) Chrome.injectFrames(webView)
172 | }
173 |
174 | fun updateScriptStorage() {
175 | val dbHelper = ScriptDbHelper(Chrome.getContext())
176 | val db = dbHelper.writableDatabase
177 | scripts.forEach {
178 | if (it.storage != null) {
179 | val values = ContentValues().apply { put("storage", it.storage.toString()) }
180 | if (db.update("script", values, "id = ?", arrayOf(it.id)).toString() == "-1") {
181 | Log.e("Updating scriptStorage failed for: " + it.id)
182 | } else {
183 | Log.d("ScriptStorage updated for " + it.id)
184 | }
185 | }
186 | }
187 | dbHelper.close()
188 | }
189 |
190 | private fun query(
191 | selection: String? = null,
192 | selectionArgs: Array? = null,
193 | ): MutableList