This commit is contained in:
Horis
2025-04-14 12:46:06 +08:00
parent cacffd18c4
commit 454341f835
7 changed files with 151 additions and 38 deletions
@@ -4,7 +4,8 @@ import android.webkit.WebSettings
import androidx.annotation.Keep import androidx.annotation.Keep
import cn.hutool.core.codec.Base64 import cn.hutool.core.codec.Base64
import cn.hutool.core.util.HexUtil import cn.hutool.core.util.HexUtil
import com.script.rhino.RhinoContext import com.script.rhino.rhinoContext
import com.script.rhino.rhinoContextOrNull
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.dateFormat import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
@@ -52,7 +53,6 @@ import kotlinx.coroutines.runBlocking
import okio.use import okio.use
import org.jsoup.Connection import org.jsoup.Connection
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.mozilla.javascript.Context
import splitties.init.appCtx import splitties.init.appCtx
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -83,9 +83,6 @@ interface JsExtensions : JsEncodeUtils {
fun getSource(): BaseSource? fun getSource(): BaseSource?
private val rhinoContext: RhinoContext
get() = Context.getCurrentContext() as RhinoContext
private val context: CoroutineContext private val context: CoroutineContext
get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext
@@ -943,7 +940,7 @@ interface JsExtensions : JsEncodeUtils {
* 输出调试日志 * 输出调试日志
*/ */
fun log(msg: Any?): Any? { fun log(msg: Any?): Any? {
rhinoContext.ensureActive() rhinoContextOrNull?.ensureActive()
getSource()?.let { getSource()?.let {
Debug.log(it.getKey(), msg.toString()) Debug.log(it.getKey(), msg.toString())
} ?: Debug.log(msg.toString()) } ?: Debug.log(msg.toString())
@@ -130,6 +130,18 @@ fun Request.Builder.get(url: String, queryMap: Map<String, String>, encoded: Boo
url(httpBuilder.build()) url(httpBuilder.build())
} }
fun Request.Builder.get(url: String, encodedQuery: String?) {
val httpBuilder = url.toHttpUrl().newBuilder()
httpBuilder.encodedQuery(encodedQuery)
url(httpBuilder.build())
}
private val formContentType = "application/x-www-form-urlencoded".toMediaType()
fun Request.Builder.postForm(encodedForm: String) {
post(encodedForm.toRequestBody(formContentType))
}
fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) { fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
form.forEach { form.forEach {
@@ -21,6 +21,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getOrPutLimit import io.legado.app.utils.getOrPutLimit
import io.legado.app.utils.isDataUrl
import io.legado.app.utils.isJson import io.legado.app.utils.isJson
import io.legado.app.utils.printOnDebug import io.legado.app.utils.printOnDebug
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
@@ -100,6 +101,9 @@ class AnalyzeRule(
} }
fun setRedirectUrl(url: String): URL? { fun setRedirectUrl(url: String): URL? {
if (url.isDataUrl()) {
return redirectUrl
}
try { try {
redirectUrl = URL(url) redirectUrl = URL(url)
} catch (e: Exception) { } catch (e: Exception) {
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.util.Base64 import android.util.Base64
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import cn.hutool.core.net.URLEncodeUtil
import cn.hutool.core.util.HexUtil import cn.hutool.core.util.HexUtil
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.script.buildScriptBindings import com.script.buildScriptBindings
@@ -47,7 +48,6 @@ import io.legado.app.utils.isJson
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isXml import io.legado.app.utils.isXml
import io.legado.app.utils.splitNotBlank
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -56,6 +56,7 @@ import okhttp3.Response
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.ContinuationInterceptor
@@ -101,8 +102,8 @@ class AnalyzeUrl(
private set private set
val headerMap = HashMap<String, String>() val headerMap = HashMap<String, String>()
private var urlNoQuery: String = "" private var urlNoQuery: String = ""
private var queryStr: String? = null private var encodedForm: String? = null
private val fieldMap = LinkedHashMap<String, String>() private var encodedQuery: String? = null
private var charset: String? = null private var charset: String? = null
private var method = RequestMethod.GET private var method = RequestMethod.GET
private var proxy: String? = null private var proxy: String? = null
@@ -251,7 +252,7 @@ class AnalyzeUrl(
RequestMethod.GET -> { RequestMethod.GET -> {
val pos = url.indexOf('?') val pos = url.indexOf('?')
if (pos != -1) { if (pos != -1) {
analyzeFields(url.substring(pos + 1)) analyzeQuery(url.substring(pos + 1))
urlNoQuery = url.substring(0, pos) urlNoQuery = url.substring(0, pos)
} }
} }
@@ -271,23 +272,68 @@ class AnalyzeUrl(
* name=<BASE64> eg name=bmFtZQ== * name=<BASE64> eg name=bmFtZQ==
*/ */
private fun analyzeFields(fieldsTxt: String) { private fun analyzeFields(fieldsTxt: String) {
queryStr = fieldsTxt encodedForm = encodeParams(fieldsTxt, charset, false)
val queryS = fieldsTxt.splitNotBlank("&") }
for (query in queryS) {
val queryPair = query.splitNotBlank("=", limit = 2) private fun analyzeQuery(query: String) {
val key = queryPair[0] encodedQuery = encodeParams(query, charset, true)
val value = queryPair.getOrNull(1) ?: "" }
if (charset.isNullOrEmpty()) {
if (NetworkUtils.hasUrlEncoded(value)) { private fun encodeParams(params: String, charset: String?, isQuery: Boolean): String {
fieldMap[key] = value val checkEncoded = charset.isNullOrEmpty()
} else { val charset = when {
fieldMap[key] = URLEncoder.encode(value, "UTF-8") charset.isNullOrEmpty() -> Charsets.UTF_8
} charset == "escape" -> null
} else if (charset == "escape") { else -> charset(charset)
fieldMap[key] = EncoderUtils.escape(value) }
} else { val len = params.length
fieldMap[key] = URLEncoder.encode(value, charset) val sb = StringBuilder()
var pos = 0
while (pos <= len) {
if (sb.isNotEmpty()) {
sb.append("&")
} }
var ampOffset = params.indexOf("&", pos)
if (ampOffset == -1) {
ampOffset = len
}
val eqOffset = params.indexOf("=", pos)
val key: String
val value: String?
if (eqOffset == -1 || eqOffset > ampOffset) {
key = params.substring(pos, ampOffset)
value = null
} else {
key = params.substring(pos, eqOffset)
value = params.substring(eqOffset + 1, ampOffset)
}
sb.appendEncoded(key, isQuery, checkEncoded, charset)
if (value != null) {
sb.append("=")
sb.appendEncoded(value, isQuery, checkEncoded, charset)
}
pos = ampOffset + 1
}
return sb.toString()
}
private fun StringBuilder.appendEncoded(
value: String,
isQuery: Boolean,
checkEncoded: Boolean,
charset: Charset?
) {
val needEncode = checkEncoded &&
((isQuery && !NetworkUtils.encodedQuery(value)) ||
(!isQuery && !NetworkUtils.encodedForm(value)))
if (!needEncode) {
append(value)
} else if (charset == null) {
append(EncoderUtils.escape(value))
} else if (isQuery) {
append(URLEncodeUtil.encodeQuery(value, charset))
} else {
append(URLEncoder.encode(value, charset))
} }
} }
@@ -356,8 +402,8 @@ class AnalyzeUrl(
val res = getClient().newCallStrResponse(retry) { val res = getClient().newCallStrResponse(retry) {
addHeaders(headerMap) addHeaders(headerMap)
url(urlNoQuery) url(urlNoQuery)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true) postForm(encodedForm ?: "")
} else { } else {
postJson(body) postJson(body)
} }
@@ -390,8 +436,8 @@ class AnalyzeUrl(
url(urlNoQuery) url(urlNoQuery)
val contentType = headerMap["Content-Type"] val contentType = headerMap["Content-Type"]
val body = body val body = body
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true) postForm(encodedForm ?: "")
} else if (!contentType.isNullOrBlank()) { } else if (!contentType.isNullOrBlank()) {
val requestBody = body.toRequestBody(contentType.toMediaType()) val requestBody = body.toRequestBody(contentType.toMediaType())
post(requestBody) post(requestBody)
@@ -400,7 +446,7 @@ class AnalyzeUrl(
} }
} }
else -> get(urlNoQuery, fieldMap, true) else -> get(urlNoQuery, encodedQuery)
} }
}.let { }.let {
val isXml = it.raw.body?.contentType()?.toString() val isXml = it.raw.body?.contentType()?.toString()
@@ -438,8 +484,8 @@ class AnalyzeUrl(
url(urlNoQuery) url(urlNoQuery)
val contentType = headerMap["Content-Type"] val contentType = headerMap["Content-Type"]
val body = body val body = body
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true) postForm(encodedForm ?: "")
} else if (!contentType.isNullOrBlank()) { } else if (!contentType.isNullOrBlank()) {
val requestBody = body.toRequestBody(contentType.toMediaType()) val requestBody = body.toRequestBody(contentType.toMediaType())
post(requestBody) post(requestBody)
@@ -448,7 +494,7 @@ class AnalyzeUrl(
} }
} }
else -> get(urlNoQuery, fieldMap, true) else -> get(urlNoQuery, encodedQuery)
} }
} }
return response return response
@@ -26,6 +26,7 @@ import io.legado.app.utils.dpToPx
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.printOnDebug import io.legado.app.utils.printOnDebug
import io.legado.app.utils.sendToClip
import io.legado.app.utils.setLayout import io.legado.app.utils.setLayout
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@@ -35,6 +36,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx
import splitties.views.onClick import splitties.views.onClick
@@ -109,6 +111,9 @@ class SourceLoginDialog : BaseDialogFragment(R.layout.dialog_login, true) {
setTitle(R.string.login_header) setTitle(R.string.login_header)
source.getLoginHeader()?.let { loginHeader -> source.getLoginHeader()?.let { loginHeader ->
setMessage(loginHeader) setMessage(loginHeader)
positiveButton(R.string.copy_text) {
appCtx.sendToClip(loginHeader)
}
} }
} }
@@ -55,7 +55,7 @@ object NetworkUtils {
return false return false
} }
private val notNeedEncoding: BitSet by lazy { private val notNeedEncodingQuery: BitSet by lazy {
val bitSet = BitSet(256) val bitSet = BitSet(256)
for (i in 'a'.code..'z'.code) { for (i in 'a'.code..'z'.code) {
bitSet.set(i) bitSet.set(i)
@@ -66,7 +66,24 @@ object NetworkUtils {
for (i in '0'.code..'9'.code) { for (i in '0'.code..'9'.code) {
bitSet.set(i) bitSet.set(i)
} }
for (char in "+-_.~$:()!*@&#,[]") { for (char in "!$&()*+,-./:;=?@[\\]^_`{|}~") {
bitSet.set(char.code)
}
return@lazy bitSet
}
private val notNeedEncodingForm: BitSet by lazy {
val bitSet = BitSet(256)
for (i in 'a'.code..'z'.code) {
bitSet.set(i)
}
for (i in 'A'.code..'Z'.code) {
bitSet.set(i)
}
for (i in '0'.code..'9'.code) {
bitSet.set(i)
}
for (char in "*-._") {
bitSet.set(char.code) bitSet.set(char.code)
} }
return@lazy bitSet return@lazy bitSet
@@ -78,12 +95,38 @@ object NetworkUtils {
* ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留 * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留
* 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F] * 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F]
*/ */
fun hasUrlEncoded(str: String): Boolean { fun encodedQuery(str: String): Boolean {
var needEncode = false var needEncode = false
var i = 0 var i = 0
while (i < str.length) { while (i < str.length) {
val c = str[i] val c = str[i]
if (notNeedEncoding.get(c.code)) { if (notNeedEncodingQuery.get(c.code)) {
i++
continue
}
if (c == '%' && i + 2 < str.length) {
// 判断是否符合urlEncode规范
val c1 = str[++i]
val c2 = str[++i]
if (isDigit16Char(c1) && isDigit16Char(c2)) {
i++
continue
}
}
// 其他字符,肯定需要urlEncode
needEncode = true
break
}
return !needEncode
}
fun encodedForm(str: String): Boolean {
var needEncode = false
var i = 0
while (i < str.length) {
val c = str[i]
if (notNeedEncodingForm.get(c.code)) {
i++ i++
continue continue
} }
@@ -11,6 +11,12 @@ import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
val rhinoContext: RhinoContext
get() = Context.getCurrentContext() as RhinoContext
val rhinoContextOrNull: RhinoContext?
get() = Context.getCurrentContext() as? RhinoContext
@Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") @Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
@OptIn(ExperimentalContracts::class) @OptIn(ExperimentalContracts::class)
inline fun <T> suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T { inline fun <T> suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T {