commit
0532f3ff62
@ -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