This commit is contained in:
Horis 2023-05-12 20:26:36 +08:00
parent 14ca962942
commit 1ebf3491f4
11 changed files with 309 additions and 66 deletions

View File

@ -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 {
onError(IOException("Too many redirect"))
request.cancel()
followRedirect = true
}
if (!followRedirect) {
onError(IOException("Too many redirect"))
} 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 {

View File

@ -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) {

View File

@ -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) {

View File

@ -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()
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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 {

View File

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

View File

@ -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"
}
}
/**

View File

@ -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("=")