Horis 3 gadi atpakaļ
vecāks
revīzija
ecf93801f9

+ 91 - 23
app/src/app/java/io/legado/app/lib/cronet/AbsCallBack.kt

@@ -2,6 +2,8 @@ package io.legado.app.lib.cronet
 
 import androidx.annotation.Keep
 import io.legado.app.help.coroutine.Coroutine
+import io.legado.app.help.http.CookieManager
+import io.legado.app.help.http.CookieManager.cookieJarHeader
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.utils.DebugLog
 import io.legado.app.utils.asIOException
@@ -11,6 +13,8 @@ import okhttp3.*
 import okhttp3.EventListener
 import okhttp3.MediaType.Companion.toMediaTypeOrNull
 import okhttp3.ResponseBody.Companion.asResponseBody
+import okhttp3.internal.http.HttpMethod
+import okhttp3.internal.http.StatusLine
 import okio.Buffer
 import okio.Source
 import okio.Timeout
@@ -29,7 +33,7 @@ import java.util.concurrent.atomic.AtomicBoolean
 
 @Keep
 abstract class AbsCallBack(
-    val originalRequest: Request,
+    var originalRequest: Request,
     val mCall: Call,
     private val eventListener: EventListener? = null,
     private val responseCallback: Callback? = null
@@ -43,6 +47,16 @@ abstract class AbsCallBack(
     private val callbackResults = ArrayBlockingQueue<CallbackResult>(2)
     private val urlResponseInfoChain = arrayListOf<UrlResponseInfo>()
     private var cancelJob: Coroutine<*>? = null
+    private var followRedirect = false
+    private var enableCookieJar = false
+
+    init {
+        if (originalRequest.header(cookieJarHeader) != null) {
+            enableCookieJar = true
+            originalRequest = originalRequest.newBuilder()
+                .removeHeader(cookieJarHeader).build()
+        }
+    }
 
 
     @Throws(IOException::class)
@@ -80,15 +94,23 @@ abstract class AbsCallBack(
         urlResponseInfoChain.add(info)
         val client = okHttpClient
         if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) {
-            request.followRedirect()
+            followRedirect = true
         } else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) {
-            request.followRedirect()
+            followRedirect = true
         } else if (okHttpClient.followRedirects) {
-            request.followRedirect()
-        } else {
+            followRedirect = true
+        }
+
+        if (!followRedirect) {
             onError(IOException("Too many redirect"))
-            request.cancel()
+        } else {
+            val response = toResponse(originalRequest, info, urlResponseInfoChain)
+            if (enableCookieJar) {
+                CookieManager.saveResponse(response)
+            }
+            originalRequest = buildRedirectRequest(response, originalRequest.method, newLocationUrl)
         }
+        request.cancel()
     }
 
 
@@ -102,29 +124,26 @@ abstract class AbsCallBack(
             request.cancel()
         }
 
-        val responseBuilder: Response.Builder
+        val response: Response
         try {
-            responseBuilder = createResponse(
-                originalRequest,
-                info,
-                CronetBodySource()
-            )
+            response = toResponse(originalRequest, info, urlResponseInfoChain, CronetBodySource())
         } catch (e: IOException) {
             request.cancel()
             cancelJob?.cancel()
             onError(e)
             return
         }
-        val newRequest = originalRequest.newBuilder().url(info.url).build()
-        val response = responseBuilder
-            .request(newRequest)
-            .priorResponse(buildPriorResponse(originalRequest, urlResponseInfoChain, info.urlChain))
-            .build()
+
+        if (enableCookieJar) {
+            CookieManager.saveResponse(response)
+        }
+
         mResponse = response
         onSuccess(response)
 
         //打印协议,用于调试
-        DebugLog.i(javaClass.simpleName, "onResponseStarted[${info.negotiatedProtocol}][${info.httpStatusCode}]${info.url}")
+        val msg = "onResponseStarted[${info.negotiatedProtocol}][${info.httpStatusCode}]${info.url}"
+        DebugLog.i(javaClass.simpleName, msg)
         if (eventListener != null) {
             eventListener.responseHeadersEnd(mCall, response)
             eventListener.responseBodyStart(mCall)
@@ -167,6 +186,16 @@ abstract class AbsCallBack(
     }
 
     override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) {
+        if (followRedirect) {
+            followRedirect = false
+            if (enableCookieJar) {
+                val newRequest = CookieManager.loadRequest(originalRequest)
+                buildRequest(newRequest, this)?.start()
+            } else {
+                buildRequest(originalRequest, this)?.start()
+            }
+            return
+        }
         canceled.set(true)
         callbackResults.add(CallbackResult(CallbackStep.ON_CANCELED))
         //DebugLog.i(javaClass.simpleName, "cancel[${info?.negotiatedProtocol}]${info?.url}")
@@ -293,15 +322,12 @@ abstract class AbsCallBack(
         private fun buildPriorResponse(
             request: Request,
             redirectResponseInfos: List<UrlResponseInfo>,
-            urlChain: List<String>
         ): Response? {
             var priorResponse: Response? = null
             if (redirectResponseInfos.isNotEmpty()) {
-                check(urlChain.size == redirectResponseInfos.size + 1) {
-                    "The number of redirects should be consistent across URLs and headers!"
-                }
                 for (i in redirectResponseInfos.indices) {
-                    val redirectedRequest = request.newBuilder().url(urlChain[i]).build()
+                    val url = redirectResponseInfos[i].url
+                    val redirectedRequest = request.newBuilder().url(url).build()
                     priorResponse = createResponse(redirectedRequest, redirectResponseInfos[i])
                         .priorResponse(priorResponse)
                         .build()
@@ -336,6 +362,48 @@ abstract class AbsCallBack(
             return bodySource.buffer()
                 .asResponseBody(contentType?.toMediaTypeOrNull(), contentLength)
         }
+
+        private fun buildRedirectRequest(
+            userResponse: Response,
+            method: String,
+            newLocationUrl: String
+        ): Request {
+            // Most redirects don't include a request body.
+            val requestBuilder = userResponse.request.newBuilder()
+            if (HttpMethod.permitsRequestBody(method)) {
+                val responseCode = userResponse.code
+                val maintainBody = HttpMethod.redirectsWithBody(method) ||
+                        responseCode == StatusLine.HTTP_PERM_REDIRECT ||
+                        responseCode == StatusLine.HTTP_TEMP_REDIRECT
+                if (HttpMethod.redirectsToGet(method) && responseCode != StatusLine.HTTP_PERM_REDIRECT && responseCode != StatusLine.HTTP_TEMP_REDIRECT) {
+                    requestBuilder.method("GET", null)
+                } else {
+                    val requestBody = if (maintainBody) userResponse.request.body else null
+                    requestBuilder.method(method, requestBody)
+                }
+                if (!maintainBody) {
+                    requestBuilder.removeHeader("Transfer-Encoding")
+                    requestBuilder.removeHeader("Content-Length")
+                    requestBuilder.removeHeader("Content-Type")
+                }
+            }
+
+            return requestBuilder.url(newLocationUrl).build()
+        }
+
+        private fun toResponse(
+            request: Request,
+            responseInfo: UrlResponseInfo,
+            redirectResponseInfos: List<UrlResponseInfo>,
+            bodySource: Source? = null
+        ): Response {
+            val responseBuilder = createResponse(request, responseInfo, bodySource)
+            val newRequest = request.newBuilder().url(responseInfo.url).build()
+            return responseBuilder
+                .request(newRequest)
+                .priorResponse(buildPriorResponse(request, redirectResponseInfos))
+                .build()
+        }
     }
 
     inner class CronetBodySource : Source {

+ 2 - 0
app/src/app/java/io/legado/app/lib/cronet/CronetHelper.kt

@@ -6,6 +6,7 @@ package io.legado.app.lib.cronet
 import androidx.annotation.Keep
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
+import io.legado.app.help.http.CookieManager.cookieJarHeader
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.utils.DebugLog
 import okhttp3.Headers
@@ -80,6 +81,7 @@ fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest? {
         setHttpMethod(request.method)//设置
         allowDirectExecutor()
         headers.forEachIndexed { index, _ ->
+            if (headers.name(index) == cookieJarHeader) return@forEachIndexed
             addHeader(headers.name(index), headers.value(index))
         }
         if (requestBody != null) {

+ 1 - 8
app/src/app/java/io/legado/app/lib/cronet/CronetInterceptor.kt

@@ -25,18 +25,11 @@ class CronetInterceptor(private val cookieJar: CookieJar) : Interceptor {
             //移除Keep-Alive,手动设置会导致400 BadRequest
             builder.removeHeader("Keep-Alive")
             builder.removeHeader("Accept-Encoding")
-            if (cookieJar != CookieJar.NO_COOKIES) {
-                val cookieStr = getCookie(original.url)
-                //设置Cookie
-                if (cookieStr.length > 3) {
-                    builder.addHeader("Cookie", cookieStr)
-                }
-            }
 
             val newReq = builder.build()
             proceedWithCronet(newReq, chain.call())?.let { response ->
                 //从Response 中保存Cookie到CookieJar
-                cookieJar.receiveHeaders(newReq.url, response.headers)
+                //cookieJar.receiveHeaders(newReq.url, response.headers)
                 response
             } ?: chain.proceed(original)
         } catch (e: Exception) {

+ 4 - 0
app/src/main/java/io/legado/app/constant/AppPattern.kt

@@ -43,4 +43,8 @@ object AppPattern {
     val notReadAloudRegex = Regex("^(\\s|\\p{C}|\\p{P}|\\p{Z}|\\p{S})+$")
 
     val xmlContentTypeRegex = "(application|text)/\\w*\\+?xml.*".toRegex()
+
+    val semicolonRegex = ";".toRegex()
+
+    val equalsRegex = "=".toRegex()
 }

+ 7 - 12
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -13,6 +13,7 @@ import io.legado.app.constant.AppLog
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.http.BackstageWebView
+import io.legado.app.help.http.CookieManager
 import io.legado.app.help.http.CookieStore
 import io.legado.app.help.http.SSLHelper
 import io.legado.app.help.http.StrResponse
@@ -286,10 +287,8 @@ interface JsExtensions : JsEncodeUtils {
             .headers(headers)
             .method(Connection.Method.GET)
             .execute()
-        val cookies = response.cookies()
-        CookieStore.mapToCookie(cookies)?.let {
-            val domain = NetworkUtils.getSubDomain(urlStr)
-            CacheManager.putMemory("${domain}_cookieJar", it)
+        if (getSource()?.enabledCookieJar == true) {
+            CookieManager.saveResponse(response)
         }
         return response
     }
@@ -305,10 +304,8 @@ interface JsExtensions : JsEncodeUtils {
             .headers(headers)
             .method(Connection.Method.HEAD)
             .execute()
-        val cookies = response.cookies()
-        CookieStore.mapToCookie(cookies)?.let {
-            val domain = NetworkUtils.getSubDomain(urlStr)
-            CacheManager.putMemory("${domain}_cookieJar", it)
+        if (getSource()?.enabledCookieJar == true) {
+            CookieManager.saveResponse(response)
         }
         return response
     }
@@ -325,10 +322,8 @@ interface JsExtensions : JsEncodeUtils {
             .headers(headers)
             .method(Connection.Method.POST)
             .execute()
-        val cookies = response.cookies()
-        CookieStore.mapToCookie(cookies)?.let {
-            val domain = NetworkUtils.getSubDomain(urlStr)
-            CacheManager.putMemory("${domain}_cookieJar", it)
+        if (getSource()?.enabledCookieJar == true) {
+            CookieManager.saveResponse(response)
         }
         return response
     }

+ 148 - 0
app/src/main/java/io/legado/app/help/http/CookieManager.kt

@@ -0,0 +1,148 @@
+package io.legado.app.help.http
+
+import io.legado.app.data.appDb
+import io.legado.app.help.CacheManager
+import io.legado.app.utils.NetworkUtils
+import okhttp3.Cookie
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.Connection
+
+object CookieManager {
+    /**
+     * <domain>_session_cookie 会话期 cookie,应用重启后失效
+     * <domain>_cookie cookies 缓存
+     */
+
+    const val cookieJarHeader = "CookieJar"
+
+    /**
+     * 从响应中保存Cookies
+     */
+    fun saveResponse(response: Response) {
+        val url = response.request.url
+        val headers = response.headers
+        saveCookiesFromHeaders(url, headers)
+    }
+
+    fun saveResponse(response: Connection.Response) {
+        val url = response.url().toHttpUrlOrNull() ?: return
+        val headers = response.multiHeaders().toHeaders()
+        saveCookiesFromHeaders(url, headers)
+    }
+
+    private fun saveCookiesFromHeaders(url: HttpUrl, headers: Headers) {
+        val domain = NetworkUtils.getSubDomain(url.toString())
+        val cookies = Cookie.parseAll(url, headers)
+
+        val sessionCookie = cookies.filter { !it.persistent }.getString()
+        updateSessionCookie(domain, sessionCookie)
+
+        val cookieString = cookies.filter { it.persistent }.getString()
+        CookieStore.replaceCookie(domain, cookieString)
+    }
+
+    /**
+     * 加载Cookies到请求中
+     */
+    fun loadRequest(request: Request): Request {
+        val domain = NetworkUtils.getSubDomain(request.url.toString())
+
+        val cookie = CookieStore.getCookie(domain)
+        val requestCookie = request.header("Cookie")
+
+        mergeCookies(cookie, requestCookie)?.let {
+            return request.newBuilder()
+                .header("Cookie", it)
+                .build()
+        }
+        return request
+    }
+
+    private fun getSessionCookieMap(domain: String): MutableMap<String, String>? {
+        return getSessionCookie(domain)?.let { CookieStore.cookieToMap(it) }
+    }
+
+    fun getSessionCookie(domain: String): String? {
+        return CacheManager.getFromMemory("${domain}_session_cookie") as? String
+    }
+
+    private fun updateSessionCookie(domain: String, cookies: String) {
+        val sessionCookie = getSessionCookie(domain)
+        if (sessionCookie.isNullOrEmpty()) {
+            CacheManager.putMemory("${domain}_session_cookie", cookies)
+            return
+        }
+
+        val ck = mergeCookies(sessionCookie, cookies) ?: return
+        CacheManager.putMemory("${domain}_session_cookie", ck)
+    }
+
+    fun mergeCookies(vararg cookies: String?): String? {
+        val cookieMap = mergeCookiesToMap(*cookies)
+        return CookieStore.mapToCookie(cookieMap)
+    }
+
+    fun mergeCookiesToMap(vararg cookies: String?): MutableMap<String, String> {
+        return cookies.filterNotNull().map {
+            CookieStore.cookieToMap(it)
+        }.reduce { acc, cookieMap ->
+            acc.apply { putAll(cookieMap) }
+        }
+    }
+
+    /**
+     * 删除单个Cookie
+     */
+    fun removeCookie(url: String, key: String) {
+        val domain = NetworkUtils.getSubDomain(url)
+
+        getSessionCookieMap(domain)?.let {
+            it.remove(key)
+            CookieStore.mapToCookie(it)?.let { cookie ->
+                CacheManager.putMemory("${domain}_session_cookie", cookie)
+            }
+        }
+
+        val cookie = getCookieNoSession(url)
+        if (cookie.isNotEmpty()) {
+            val cookieMap = CookieStore.cookieToMap(cookie).apply { remove(key) }
+            CookieStore.mapToCookie(cookieMap)?.let {
+                CookieStore.setCookie(url, it)
+            }
+        }
+    }
+
+    fun getCookieNoSession(url: String): String {
+        val domain = NetworkUtils.getSubDomain(url)
+        val cacheCookie = CacheManager.getFromMemory("${domain}_cookie") as? String
+
+        return if (cacheCookie != null) {
+            cacheCookie
+        } else {
+            val cookieBean = appDb.cookieDao.get(domain)
+            cookieBean?.cookie ?: ""
+        }
+    }
+
+    fun List<Cookie>.getString() = buildString {
+        this@getString.forEachIndexed { index, cookie ->
+            if (index > 0) append("; ")
+            append(cookie.name).append('=').append(cookie.value)
+        }
+    }
+
+    private fun Map<String, List<String>>.toHeaders(): Headers {
+        return Headers.Builder().apply {
+            this@toHeaders.forEach { (k, v) ->
+                v.forEach {
+                    add(k, it)
+                }
+            }
+        }.build()
+    }
+
+}

+ 26 - 14
app/src/main/java/io/legado/app/help/http/CookieStore.kt

@@ -3,14 +3,18 @@
 package io.legado.app.help.http
 
 import android.text.TextUtils
+import io.legado.app.constant.AppPattern.semicolonRegex
+import io.legado.app.constant.AppPattern.equalsRegex
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Cookie
 import io.legado.app.help.CacheManager
-import io.legado.app.help.http.api.CookieManager
+import io.legado.app.help.http.CookieManager.getCookieNoSession
+import io.legado.app.help.http.CookieManager.mergeCookiesToMap
+import io.legado.app.help.http.api.CookieManagerInterface
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.removeCookie
 
-object CookieStore : CookieManager {
+object CookieStore : CookieManagerInterface {
 
     /**
      *保存cookie到数据库,会自动识别url的二级域名
@@ -26,7 +30,7 @@ object CookieStore : CookieManager {
         if (TextUtils.isEmpty(url) || TextUtils.isEmpty(cookie)) {
             return
         }
-        val oldCookie = getCookie(url)
+        val oldCookie = getCookieNoSession(url)
         if (TextUtils.isEmpty(oldCookie)) {
             setCookie(url, cookie)
         } else {
@@ -42,19 +46,26 @@ object CookieStore : CookieManager {
      */
     override fun getCookie(url: String): String {
         val domain = NetworkUtils.getSubDomain(url)
-        CacheManager.getFromMemory("${domain}_cookie")?.let {
-            if (it is String) return it
-        }
 
-        val cookieBean = appDb.cookieDao.get(domain)
-        val cookie = cookieBean?.cookie ?: ""
-        CacheManager.putMemory(url, cookie)
-        return cookie
+        val cookie = getCookieNoSession(url)
+        val sessionCookie = CookieManager.getSessionCookie(domain)
+
+        val cookieMap = mergeCookiesToMap(cookie, sessionCookie)
+
+        var ck = mapToCookie(cookieMap) ?: ""
+        while (ck.length > 4096) {
+            val removeKey = cookieMap.keys.random()
+            CookieManager.removeCookie(url, removeKey)
+            cookieMap.remove(removeKey)
+            ck = mapToCookie(cookieMap) ?: ""
+        }
+        return ck
     }
 
     fun getKey(url: String, key: String): String {
         val cookie = getCookie(url)
-        val cookieMap = cookieToMap(cookie)
+        val sessionCookie = CookieManager.getSessionCookie(url)
+        val cookieMap = mergeCookiesToMap(cookie, sessionCookie)
         return cookieMap[key] ?: ""
     }
 
@@ -62,6 +73,7 @@ object CookieStore : CookieManager {
         val domain = NetworkUtils.getSubDomain(url)
         appDb.cookieDao.delete(domain)
         CacheManager.deleteMemory("${domain}_cookie")
+        CacheManager.deleteMemory("${domain}_session_cookie")
         android.webkit.CookieManager.getInstance().removeCookie(domain)
     }
 
@@ -70,9 +82,9 @@ object CookieStore : CookieManager {
         if (cookie.isBlank()) {
             return cookieMap
         }
-        val pairArray = cookie.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+        val pairArray = cookie.split(semicolonRegex).dropLastWhile { it.isEmpty() }.toTypedArray()
         for (pair in pairArray) {
-            val pairs = pair.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+            val pairs = pair.split(equalsRegex).dropLastWhile { it.isEmpty() }.toTypedArray()
             if (pairs.size == 1) {
                 continue
             }
@@ -91,7 +103,7 @@ object CookieStore : CookieManager {
         }
         val builder = StringBuilder()
         cookieMap.keys.forEachIndexed { index, key ->
-            if (index > 0) builder.append(";")
+            if (index > 0) builder.append("; ")
             builder.append(key).append("=").append(cookieMap[key])
         }
         return builder.toString()

+ 21 - 2
app/src/main/java/io/legado/app/help/http/HttpHelper.kt

@@ -3,6 +3,7 @@ package io.legado.app.help.http
 import io.legado.app.constant.AppConst
 import io.legado.app.help.CacheManager
 import io.legado.app.help.config.AppConfig
+import io.legado.app.help.http.CookieManager.cookieJarHeader
 import io.legado.app.utils.NetworkUtils
 import okhttp3.*
 import java.net.InetSocketAddress
@@ -48,7 +49,7 @@ val okHttpClient: OkHttpClient by lazy {
         .writeTimeout(15, TimeUnit.SECONDS)
         .readTimeout(15, TimeUnit.SECONDS)
         .callTimeout(60, TimeUnit.SECONDS)
-        .cookieJar(cookieJar = cookieJar)
+        //.cookieJar(cookieJar = cookieJar)
         .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager)
         .retryOnConnectionFailure(true)
         .hostnameVerifier(SSLHelper.unsafeHostnameVerifier)
@@ -66,8 +67,26 @@ val okHttpClient: OkHttpClient by lazy {
             builder.addHeader("Keep-Alive", "300")
             builder.addHeader("Connection", "Keep-Alive")
             builder.addHeader("Cache-Control", "no-cache")
-            chain.proceed(builder.build())
+            chain.proceed(builder.build()).newBuilder().removeHeader(cookieJarHeader).build()
         })
+        .addNetworkInterceptor { chain ->
+            var request = chain.request()
+            val enableCookieJar = request.header(cookieJarHeader) !=  null
+
+            if (enableCookieJar) {
+                val requestBuilder = request.newBuilder()
+                requestBuilder.removeHeader(cookieJarHeader)
+                request = CookieManager.loadRequest(requestBuilder.build())
+            }
+
+            var networkResponse = chain.proceed(request)
+
+            if (enableCookieJar) {
+                CookieManager.saveResponse(networkResponse)
+                networkResponse = networkResponse.newBuilder().header(cookieJarHeader, "1").build()
+            }
+            networkResponse
+        }
     if (!AppConst.isPlayChannel && AppConfig.isCronet) {
         if (Cronet.loader?.install() == true) {
             Cronet.interceptor?.let {

+ 1 - 1
app/src/main/java/io/legado/app/help/http/api/CookieManager.kt → app/src/main/java/io/legado/app/help/http/api/CookieManagerInterface.kt

@@ -1,6 +1,6 @@
 package io.legado.app.help.http.api
 
-interface CookieManager {
+interface CookieManagerInterface {
 
     /**
      * 保存cookie

+ 5 - 4
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt

@@ -20,6 +20,7 @@ import io.legado.app.help.JsExtensions
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.glide.GlideHeaders
 import io.legado.app.help.http.*
+import io.legado.app.help.http.CookieManager.mergeCookies
 import io.legado.app.utils.*
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
@@ -606,13 +607,13 @@ class AnalyzeUrl(
             CookieStore.getCookie(domain)
         }
         if (cookie.isNotEmpty()) {
-            val cookieMap = CookieStore.cookieToMap(cookie)
-            val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
-            cookieMap.putAll(customCookieMap)
-            CookieStore.mapToCookie(cookieMap)?.let {
+            mergeCookies(cookie, headerMap["Cookie"])?.let {
                 headerMap.put("Cookie", it)
             }
         }
+        if (enabledCookieJar) {
+            headerMap[CookieManager.cookieJarHeader] = "1"
+        }
     }
 
     /**

+ 2 - 1
app/src/main/java/io/legado/app/utils/UrlUtil.kt

@@ -2,6 +2,7 @@ package io.legado.app.utils
 
 import io.legado.app.BuildConfig
 import io.legado.app.constant.AppLog
+import io.legado.app.constant.AppPattern.semicolonRegex
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.CustomUrl
@@ -102,7 +103,7 @@ object UrlUtil {
         val redirectUrl: String? = conn.getHeaderField("Location")
 
         return if (raw != null) {
-            val fileNames = raw.split(";".toRegex()).filter { it.contains("filename") }
+            val fileNames = raw.split(semicolonRegex).filter { it.contains("filename") }
             val names = hashSetOf<String>()
             fileNames.forEach {
                 val fileName = it.substringAfter("=")