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 cn.hutool.core.codec.Base64
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.dateFormat
import io.legado.app.constant.AppLog
@@ -52,7 +53,6 @@ import kotlinx.coroutines.runBlocking
import okio.use
import org.jsoup.Connection
import org.jsoup.Jsoup
import org.mozilla.javascript.Context
import splitties.init.appCtx
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -83,9 +83,6 @@ interface JsExtensions : JsEncodeUtils {
fun getSource(): BaseSource?
private val rhinoContext: RhinoContext
get() = Context.getCurrentContext() as RhinoContext
private val context: CoroutineContext
get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext
@@ -943,7 +940,7 @@ interface JsExtensions : JsEncodeUtils {
* 输出调试日志
*/
fun log(msg: Any?): Any? {
rhinoContext.ensureActive()
rhinoContextOrNull?.ensureActive()
getSource()?.let {
Debug.log(it.getKey(), 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())
}
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) {
val formBody = FormBody.Builder()
form.forEach {
@@ -21,6 +21,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getOrPutLimit
import io.legado.app.utils.isDataUrl
import io.legado.app.utils.isJson
import io.legado.app.utils.printOnDebug
import io.legado.app.utils.splitNotBlank
@@ -100,6 +101,9 @@ class AnalyzeRule(
}
fun setRedirectUrl(url: String): URL? {
if (url.isDataUrl()) {
return redirectUrl
}
try {
redirectUrl = URL(url)
} catch (e: Exception) {
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.util.Base64
import androidx.annotation.Keep
import androidx.media3.common.MediaItem
import cn.hutool.core.net.URLEncodeUtil
import cn.hutool.core.util.HexUtil
import com.bumptech.glide.load.model.GlideUrl
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.isJsonObject
import io.legado.app.utils.isXml
import io.legado.app.utils.splitNotBlank
import kotlinx.coroutines.runBlocking
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@@ -56,6 +56,7 @@ import okhttp3.Response
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.net.URLEncoder
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import kotlin.coroutines.ContinuationInterceptor
@@ -101,8 +102,8 @@ class AnalyzeUrl(
private set
val headerMap = HashMap<String, String>()
private var urlNoQuery: String = ""
private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>()
private var encodedForm: String? = null
private var encodedQuery: String? = null
private var charset: String? = null
private var method = RequestMethod.GET
private var proxy: String? = null
@@ -251,7 +252,7 @@ class AnalyzeUrl(
RequestMethod.GET -> {
val pos = url.indexOf('?')
if (pos != -1) {
analyzeFields(url.substring(pos + 1))
analyzeQuery(url.substring(pos + 1))
urlNoQuery = url.substring(0, pos)
}
}
@@ -271,23 +272,68 @@ class AnalyzeUrl(
* name=<BASE64> eg name=bmFtZQ==
*/
private fun analyzeFields(fieldsTxt: String) {
queryStr = fieldsTxt
val queryS = fieldsTxt.splitNotBlank("&")
for (query in queryS) {
val queryPair = query.splitNotBlank("=", limit = 2)
val key = queryPair[0]
val value = queryPair.getOrNull(1) ?: ""
if (charset.isNullOrEmpty()) {
if (NetworkUtils.hasUrlEncoded(value)) {
fieldMap[key] = value
} else {
fieldMap[key] = URLEncoder.encode(value, "UTF-8")
}
} else if (charset == "escape") {
fieldMap[key] = EncoderUtils.escape(value)
} else {
fieldMap[key] = URLEncoder.encode(value, charset)
encodedForm = encodeParams(fieldsTxt, charset, false)
}
private fun analyzeQuery(query: String) {
encodedQuery = encodeParams(query, charset, true)
}
private fun encodeParams(params: String, charset: String?, isQuery: Boolean): String {
val checkEncoded = charset.isNullOrEmpty()
val charset = when {
charset.isNullOrEmpty() -> Charsets.UTF_8
charset == "escape" -> null
else -> charset(charset)
}
val len = params.length
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) {
addHeaders(headerMap)
url(urlNoQuery)
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
postForm(encodedForm ?: "")
} else {
postJson(body)
}
@@ -390,8 +436,8 @@ class AnalyzeUrl(
url(urlNoQuery)
val contentType = headerMap["Content-Type"]
val body = body
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
postForm(encodedForm ?: "")
} else if (!contentType.isNullOrBlank()) {
val requestBody = body.toRequestBody(contentType.toMediaType())
post(requestBody)
@@ -400,7 +446,7 @@ class AnalyzeUrl(
}
}
else -> get(urlNoQuery, fieldMap, true)
else -> get(urlNoQuery, encodedQuery)
}
}.let {
val isXml = it.raw.body?.contentType()?.toString()
@@ -438,8 +484,8 @@ class AnalyzeUrl(
url(urlNoQuery)
val contentType = headerMap["Content-Type"]
val body = body
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) {
postForm(encodedForm ?: "")
} else if (!contentType.isNullOrBlank()) {
val requestBody = body.toRequestBody(contentType.toMediaType())
post(requestBody)
@@ -448,7 +494,7 @@ class AnalyzeUrl(
}
}
else -> get(urlNoQuery, fieldMap, true)
else -> get(urlNoQuery, encodedQuery)
}
}
return response
@@ -26,6 +26,7 @@ import io.legado.app.utils.dpToPx
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.openUrl
import io.legado.app.utils.printOnDebug
import io.legado.app.utils.sendToClip
import io.legado.app.utils.setLayout
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.toastOnUi
@@ -35,6 +36,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import splitties.views.onClick
@@ -109,6 +111,9 @@ class SourceLoginDialog : BaseDialogFragment(R.layout.dialog_login, true) {
setTitle(R.string.login_header)
source.getLoginHeader()?.let { loginHeader ->
setMessage(loginHeader)
positiveButton(R.string.copy_text) {
appCtx.sendToClip(loginHeader)
}
}
}
@@ -55,7 +55,7 @@ object NetworkUtils {
return false
}
private val notNeedEncoding: BitSet by lazy {
private val notNeedEncodingQuery: BitSet by lazy {
val bitSet = BitSet(256)
for (i in 'a'.code..'z'.code) {
bitSet.set(i)
@@ -66,7 +66,24 @@ object NetworkUtils {
for (i in '0'.code..'9'.code) {
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)
}
return@lazy bitSet
@@ -78,12 +95,38 @@ object NetworkUtils {
* ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留
* 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F]
*/
fun hasUrlEncoded(str: String): Boolean {
fun encodedQuery(str: String): Boolean {
var needEncode = false
var i = 0
while (i < str.length) {
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++
continue
}
@@ -11,6 +11,12 @@ import kotlin.coroutines.ContinuationInterceptor
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")
@OptIn(ExperimentalContracts::class)
inline fun <T> suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T {