parent
87e279ecf4
commit
7aaf9bdf5e
@ -0,0 +1,65 @@ |
|||||||
|
package io.legado.app.help.http.cronet |
||||||
|
|
||||||
|
import io.legado.app.help.http.CookieStore |
||||||
|
import okhttp3.Headers |
||||||
|
import okhttp3.MediaType |
||||||
|
import okhttp3.Request |
||||||
|
import okio.Buffer |
||||||
|
import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK |
||||||
|
import org.chromium.net.ExperimentalCronetEngine |
||||||
|
import org.chromium.net.UploadDataProviders |
||||||
|
import org.chromium.net.UrlRequest |
||||||
|
import splitties.init.appCtx |
||||||
|
import java.util.concurrent.Executor |
||||||
|
import java.util.concurrent.Executors |
||||||
|
|
||||||
|
|
||||||
|
val executor: Executor by lazy { Executors.newSingleThreadExecutor() } |
||||||
|
|
||||||
|
val cronetEngine: ExperimentalCronetEngine by lazy { |
||||||
|
|
||||||
|
val builder = ExperimentalCronetEngine.Builder(appCtx) |
||||||
|
.setStoragePath(appCtx.externalCacheDir?.absolutePath) |
||||||
|
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50)) |
||||||
|
.enableQuic(true) |
||||||
|
.enablePublicKeyPinningBypassForLocalTrustAnchors(true) |
||||||
|
.enableHttp2(true) |
||||||
|
//Brotli压缩 |
||||||
|
builder.enableBrotli(true) |
||||||
|
return@lazy builder.build() |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest { |
||||||
|
val url = request.url.toString() |
||||||
|
val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor) |
||||||
|
requestBuilder.setHttpMethod(request.method) |
||||||
|
val cookie = CookieStore.getCookie(url) |
||||||
|
if (cookie.length > 1) { |
||||||
|
requestBuilder.addHeader("Cookie", cookie) |
||||||
|
} |
||||||
|
val headers: Headers = request.headers |
||||||
|
headers.forEachIndexed { index, pair -> |
||||||
|
requestBuilder.addHeader(headers.name(index), headers.value(index)) |
||||||
|
} |
||||||
|
|
||||||
|
val requestBody = request.body |
||||||
|
if (requestBody != null) { |
||||||
|
val contentType: MediaType? = requestBody.contentType() |
||||||
|
if (contentType != null) { |
||||||
|
requestBuilder.addHeader("Content-Type", contentType.toString()) |
||||||
|
} |
||||||
|
val buffer = Buffer() |
||||||
|
requestBody.writeTo(buffer) |
||||||
|
requestBuilder.setUploadDataProvider( |
||||||
|
UploadDataProviders.create(buffer.readByteArray()), |
||||||
|
executor |
||||||
|
); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return requestBuilder.build() |
||||||
|
|
||||||
|
} |
||||||
|
|
@ -0,0 +1,20 @@ |
|||||||
|
package io.legado.app.help.http.cronet |
||||||
|
|
||||||
|
import okhttp3.* |
||||||
|
import java.io.IOException |
||||||
|
|
||||||
|
class CronetInterceptor : Interceptor { |
||||||
|
@Throws(IOException::class) |
||||||
|
override fun intercept(chain: Interceptor.Chain): Response { |
||||||
|
return proceedWithCronet(chain.request(), chain.call()) |
||||||
|
} |
||||||
|
|
||||||
|
@Throws(IOException::class) |
||||||
|
private fun proceedWithCronet(request: Request, call: Call): Response { |
||||||
|
val callback = CronetUrlRequestCallback(request, call) |
||||||
|
val urlRequest = buildRequest(request, callback) |
||||||
|
urlRequest.start() |
||||||
|
return callback.waitForDone() |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,207 @@ |
|||||||
|
package io.legado.app.help.http.cronet |
||||||
|
|
||||||
|
import android.os.ConditionVariable |
||||||
|
import android.util.Log |
||||||
|
import io.legado.app.help.http.okHttpClient |
||||||
|
import okhttp3.* |
||||||
|
import okhttp3.EventListener |
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull |
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody |
||||||
|
import org.chromium.net.CronetException |
||||||
|
import org.chromium.net.UrlRequest |
||||||
|
import org.chromium.net.UrlResponseInfo |
||||||
|
import java.io.ByteArrayOutputStream |
||||||
|
import java.io.IOException |
||||||
|
import java.nio.ByteBuffer |
||||||
|
import java.nio.channels.Channels |
||||||
|
import java.util.* |
||||||
|
|
||||||
|
class CronetUrlRequestCallback @JvmOverloads internal constructor( |
||||||
|
private val originalRequest: Request, |
||||||
|
private val mCall: Call, |
||||||
|
eventListener: EventListener? = null, |
||||||
|
responseCallback: Callback? = null |
||||||
|
) : UrlRequest.Callback() { |
||||||
|
private val eventListener: EventListener? |
||||||
|
private val responseCallback: Callback? |
||||||
|
private var followCount = 0 |
||||||
|
private var mResponse: Response |
||||||
|
private var mException: IOException? = null |
||||||
|
private val mResponseCondition = ConditionVariable() |
||||||
|
private val mBytesReceived = ByteArrayOutputStream() |
||||||
|
private val mReceiveChannel = Channels.newChannel(mBytesReceived) |
||||||
|
|
||||||
|
@Throws(IOException::class) |
||||||
|
fun waitForDone(): Response { |
||||||
|
mResponseCondition.block() |
||||||
|
if (mException != null) { |
||||||
|
throw mException as IOException |
||||||
|
} |
||||||
|
return mResponse |
||||||
|
} |
||||||
|
|
||||||
|
override fun onRedirectReceived( |
||||||
|
request: UrlRequest, |
||||||
|
info: UrlResponseInfo, |
||||||
|
newLocationUrl: String |
||||||
|
) { |
||||||
|
if (followCount > MAX_FOLLOW_COUNT) { |
||||||
|
request.cancel() |
||||||
|
} |
||||||
|
followCount += 1 |
||||||
|
val client = okHttpClient |
||||||
|
if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) { |
||||||
|
request.followRedirect() |
||||||
|
} else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) { |
||||||
|
request.followRedirect() |
||||||
|
} else if (client.followRedirects) { |
||||||
|
request.followRedirect() |
||||||
|
} else { |
||||||
|
request.cancel() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) { |
||||||
|
mResponse = responseFromResponse(mResponse, info) |
||||||
|
// val sb: StringBuilder = StringBuilder(info.url).append("\r\n") |
||||||
|
// sb.append("[Cached:").append(info.wasCached()).append("][StatusCode:") |
||||||
|
// .append(info.httpStatusCode).append("][StatusText:").append(info.httpStatusText) |
||||||
|
// .append("][Protocol:").append(info.negotiatedProtocol).append("][ByteCount:") |
||||||
|
// .append(info.receivedByteCount).append("]\r\n"); |
||||||
|
// val httpHeaders=info.allHeadersAsList |
||||||
|
// httpHeaders.forEach { h -> |
||||||
|
// sb.append("[").append(h.key).append("]").append(h.value).append("\r\n"); |
||||||
|
// } |
||||||
|
// Log.e("Cronet", sb.toString()) |
||||||
|
if (eventListener != null) { |
||||||
|
eventListener.responseHeadersEnd(mCall, mResponse) |
||||||
|
eventListener.responseBodyStart(mCall) |
||||||
|
} |
||||||
|
request.read(ByteBuffer.allocateDirect(32 * 1024)) |
||||||
|
} |
||||||
|
|
||||||
|
@Throws(Exception::class) |
||||||
|
override fun onReadCompleted( |
||||||
|
request: UrlRequest, |
||||||
|
info: UrlResponseInfo, |
||||||
|
byteBuffer: ByteBuffer |
||||||
|
) { |
||||||
|
byteBuffer.flip() |
||||||
|
try { |
||||||
|
mReceiveChannel.write(byteBuffer) |
||||||
|
} catch (e: IOException) { |
||||||
|
Log.i(TAG, "IOException during ByteBuffer read. Details: ", e) |
||||||
|
throw e |
||||||
|
} |
||||||
|
byteBuffer.clear() |
||||||
|
request.read(byteBuffer) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { |
||||||
|
eventListener?.responseBodyEnd(mCall, info.receivedByteCount) |
||||||
|
val contentType: MediaType? = (mResponse.header("content-type") |
||||||
|
?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull() |
||||||
|
val responseBody: ResponseBody = |
||||||
|
mBytesReceived.toByteArray().toResponseBody(contentType) |
||||||
|
val newRequest = originalRequest.newBuilder().url(info.url).build() |
||||||
|
mResponse = mResponse.newBuilder().body(responseBody).request(newRequest).build() |
||||||
|
mResponseCondition.open() |
||||||
|
eventListener?.callEnd(mCall) |
||||||
|
if (responseCallback != null) { |
||||||
|
try { |
||||||
|
responseCallback.onResponse(mCall, mResponse) |
||||||
|
} catch (e: IOException) { |
||||||
|
// Pass? |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onFailed(request: UrlRequest, info: UrlResponseInfo, error: CronetException) { |
||||||
|
val e = IOException("Cronet Exception Occurred", error) |
||||||
|
mException = e |
||||||
|
mResponseCondition.open() |
||||||
|
eventListener?.callFailed(mCall, e) |
||||||
|
responseCallback?.onFailure(mCall, e) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCanceled(request: UrlRequest, info: UrlResponseInfo) { |
||||||
|
mResponseCondition.open() |
||||||
|
eventListener?.callEnd(mCall) |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
private const val TAG = "Callback" |
||||||
|
private const val MAX_FOLLOW_COUNT = 20 |
||||||
|
private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol { |
||||||
|
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault()) |
||||||
|
// Log.e("Cronet", responseInfo.url) |
||||||
|
// Log.e("Cronet", negotiatedProtocol) |
||||||
|
|
||||||
|
return when { |
||||||
|
negotiatedProtocol.contains("h3") -> { |
||||||
|
return Protocol.QUIC |
||||||
|
} |
||||||
|
negotiatedProtocol.contains("quic") -> { |
||||||
|
Protocol.QUIC |
||||||
|
} |
||||||
|
negotiatedProtocol.contains("spdy") -> { |
||||||
|
Protocol.SPDY_3 |
||||||
|
} |
||||||
|
negotiatedProtocol.contains("h2") -> { |
||||||
|
Protocol.HTTP_2 |
||||||
|
} |
||||||
|
negotiatedProtocol.contains("1.1") -> { |
||||||
|
Protocol.HTTP_1_1 |
||||||
|
} |
||||||
|
else -> { |
||||||
|
Protocol.HTTP_1_0 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun headersFromResponse(responseInfo: UrlResponseInfo): Headers { |
||||||
|
val headers = responseInfo.allHeadersAsList |
||||||
|
val headerBuilder = Headers.Builder() |
||||||
|
for ((key, value) in headers) { |
||||||
|
try { |
||||||
|
if (key.equals("content-encoding", ignoreCase = true)) { |
||||||
|
// Strip all content encoding headers as decoding is done handled by cronet |
||||||
|
continue |
||||||
|
} |
||||||
|
headerBuilder.add(key, value) |
||||||
|
} catch (e: Exception) { |
||||||
|
Log.w(TAG, "Invalid HTTP header/value: $key$value") |
||||||
|
// Ignore that header |
||||||
|
} |
||||||
|
} |
||||||
|
return headerBuilder.build() |
||||||
|
} |
||||||
|
|
||||||
|
private fun responseFromResponse( |
||||||
|
response: Response, |
||||||
|
responseInfo: UrlResponseInfo |
||||||
|
): Response { |
||||||
|
val protocol = protocolFromNegotiatedProtocol(responseInfo) |
||||||
|
val headers = headersFromResponse(responseInfo) |
||||||
|
return response.newBuilder() |
||||||
|
.receivedResponseAtMillis(System.currentTimeMillis()) |
||||||
|
.protocol(protocol) |
||||||
|
.code(responseInfo.httpStatusCode) |
||||||
|
.message(responseInfo.httpStatusText) |
||||||
|
.headers(headers) |
||||||
|
.build() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
init { |
||||||
|
mResponse = Response.Builder() |
||||||
|
.sentRequestAtMillis(System.currentTimeMillis()) |
||||||
|
.request(originalRequest) |
||||||
|
.protocol(Protocol.HTTP_1_0) |
||||||
|
.code(0) |
||||||
|
.message("") |
||||||
|
.build() |
||||||
|
this.responseCallback = responseCallback |
||||||
|
this.eventListener = eventListener |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue