From 37cd97c4513eec7a08125ca9c89c490ab71f1e8a Mon Sep 17 00:00:00 2001 From: Horis <8674809+821938089@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:46:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/help/JsExtensions.kt | 9 +- .../io/legado/app/help/http/OkHttpUtils.kt | 12 +++ .../app/model/analyzeRule/AnalyzeRule.kt | 4 + .../app/model/analyzeRule/AnalyzeUrl.kt | 102 +++++++++++++----- .../legado/app/ui/login/SourceLoginDialog.kt | 5 + .../java/io/legado/app/utils/NetworkUtils.kt | 51 ++++++++- .../java/com/script/rhino/RhinoExtensions.kt | 6 ++ 7 files changed, 151 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index 5d28954ce..a029f5697 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -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()) diff --git a/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt index 38bed2203..1134967e8 100644 --- a/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt +++ b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt @@ -130,6 +130,18 @@ fun Request.Builder.get(url: String, queryMap: Map, 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, encoded: Boolean = false) { val formBody = FormBody.Builder() form.forEach { diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index cdbacd26c..87f804fe7 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -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) { diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index 13153ce2c..cca95ee9f 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -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() private var urlNoQuery: String = "" - private var queryStr: String? = null - private val fieldMap = LinkedHashMap() + 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= 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 diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt b/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt index f3d7b7a98..abbf54e4f 100644 --- a/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt +++ b/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt @@ -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) + } } } diff --git a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt index 2062a6d5c..0be4bda13 100644 --- a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -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 } diff --git a/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt b/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt index 112443ba7..f1ce0835a 100644 --- a/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt +++ b/modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt @@ -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 suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T {