优化
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user