AquaStream/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt

323 lines
13 KiB
Kotlin
Raw Normal View History

package com.lagradost.cloudstream3.network
import android.annotation.SuppressLint
import android.net.http.SslError
import android.os.Handler
import android.os.Looper
import android.webkit.*
import com.lagradost.cloudstream3.AcraApplication
2022-08-21 18:14:26 +00:00
import com.lagradost.cloudstream3.AcraApplication.Companion.context
2021-10-28 11:28:19 +00:00
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
2022-10-23 16:59:01 +00:00
import com.lagradost.cloudstream3.mvvm.debugException
2022-02-18 21:20:35 +00:00
import com.lagradost.cloudstream3.mvvm.logError
2022-10-23 16:59:01 +00:00
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.main
2022-08-21 18:14:26 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
2022-10-18 22:14:15 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.nicehttp.requestCreator
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
2021-10-28 11:28:19 +00:00
import java.net.URI
2022-01-16 15:35:06 +00:00
/**
* When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...)
* @param interceptUrl will stop the WebView when reaching this url.
* @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex.
* @param userAgent if null then will use the default user agent
2022-08-21 18:14:26 +00:00
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
* @param script pass custom js to execute
* @param scriptCallback will be called with the result from custom js
2024-02-19 20:18:36 +00:00
* @param timeout close webview after timeout
2022-01-16 15:35:06 +00:00
* */
class WebViewResolver(
val interceptUrl: Regex,
val additionalUrls: List<Regex> = emptyList(),
val userAgent: String? = USER_AGENT,
val useOkhttp: Boolean = true,
val script: String? = null,
2024-02-19 20:18:36 +00:00
val scriptCallback: ((String) -> Unit)? = null,
val timeout: Long = DEFAULT_TIMEOUT
) :
2022-01-16 15:35:06 +00:00
Interceptor {
2024-02-19 20:18:36 +00:00
constructor(
interceptUrl: Regex,
additionalUrls: List<Regex> = emptyList(),
userAgent: String? = USER_AGENT,
useOkhttp: Boolean = true,
script: String? = null,
scriptCallback: ((String) -> Unit)? = null,
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT)
constructor(
interceptUrl: Regex,
additionalUrls: List<Regex> = emptyList(),
userAgent: String? = USER_AGENT,
useOkhttp: Boolean = true
2024-02-19 20:18:36 +00:00
) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT)
2022-08-21 18:14:26 +00:00
companion object {
2024-02-19 20:18:36 +00:00
private const val DEFAULT_TIMEOUT = 60_000L
2022-08-21 18:14:26 +00:00
var webViewUserAgent: String? = null
@JvmName("getWebViewUserAgent1")
fun getWebViewUserAgent(): String? {
return webViewUserAgent ?: context?.let { ctx ->
runBlocking {
mainWork {
WebView(ctx).settings.userAgentString.also { userAgent ->
webViewUserAgent = userAgent
}
}
}
}
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return runBlocking {
2022-01-16 15:35:06 +00:00
val fixedRequest = resolveUsingWebView(request).first
return@runBlocking chain.proceed(fixedRequest ?: request)
}
}
2022-07-17 20:08:40 +00:00
suspend fun resolveUsingWebView(
url: String,
referer: String? = null,
method: String = "GET",
requestCallBack: (Request) -> Boolean = { false },
): Pair<Request?, List<Request>> {
2022-10-23 16:59:01 +00:00
return try {
resolveUsingWebView(
requestCreator(method, url, referer = referer), requestCallBack
)
} catch (e: java.lang.IllegalArgumentException) {
logError(e)
debugException { "ILLEGAL URL IN resolveUsingWebView!" }
return null to emptyList()
}
2022-07-17 20:08:40 +00:00
}
2022-01-16 15:35:06 +00:00
/**
2022-02-11 09:17:04 +00:00
* @param requestCallBack asynchronously return matched requests by either interceptUrl or additionalUrls. If true, destroy WebView.
2022-01-16 15:35:06 +00:00
* @return the final request (by interceptUrl) and all the collected urls (by additionalUrls).
* */
@SuppressLint("SetJavaScriptEnabled")
2022-01-16 15:35:06 +00:00
suspend fun resolveUsingWebView(
request: Request,
2022-02-11 09:17:04 +00:00
requestCallBack: (Request) -> Boolean = { false }
2022-01-16 15:35:06 +00:00
): Pair<Request?, List<Request>> {
val url = request.url.toString()
2021-10-10 20:01:02 +00:00
val headers = request.headers
println("Initial web-view request: $url")
var webView: WebView? = null
2022-08-21 18:38:53 +00:00
// Extra assurance it exits as it should.
var shouldExit = false
fun destroyWebView() {
main {
webView?.stopLoading()
webView?.destroy()
webView = null
2022-08-21 18:38:53 +00:00
shouldExit = true
println("Destroyed webview")
}
}
var fixedRequest: Request? = null
2022-10-18 22:14:15 +00:00
val extraRequestList = threadSafeListOf<Request>()
main {
// Useful for debugging
2022-08-21 18:14:26 +00:00
WebView.setWebContentsDebuggingEnabled(true)
2022-02-18 21:20:35 +00:00
try {
webView = WebView(
AcraApplication.context
?: throw RuntimeException("No base context in WebViewResolver")
).apply {
// Bare minimum to bypass captcha
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
2022-08-21 18:14:26 +00:00
webViewUserAgent = settings.userAgentString
// Don't set user agent, setting user agent will make cloudflare break.
if (userAgent != null) {
settings.userAgentString = userAgent
}
2022-02-18 21:20:35 +00:00
// Blocks unnecessary images, remove if captcha fucks.
2022-08-21 18:14:26 +00:00
// settings.blockNetworkImage = true
2022-02-18 21:20:35 +00:00
}
2022-02-18 21:20:35 +00:00
webView?.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? = runBlocking {
val webViewUrl = request.url.toString()
2022-08-21 18:14:26 +00:00
println("Loading WebView URL: $webViewUrl")
2021-10-18 16:04:05 +00:00
if (script != null) {
val handler = Handler(Looper.getMainLooper())
handler.post {
view.evaluateJavascript("$script")
{ scriptCallback?.invoke(it) }
}
}
2022-02-18 21:20:35 +00:00
if (interceptUrl.containsMatchIn(webViewUrl)) {
2022-10-23 16:59:01 +00:00
fixedRequest = request.toRequest()?.also {
2022-08-21 18:14:26 +00:00
requestCallBack(it)
2022-02-18 21:20:35 +00:00
}
println("Web-view request finished: $webViewUrl")
destroyWebView()
return@runBlocking null
2022-02-11 09:17:04 +00:00
}
2021-10-18 16:04:05 +00:00
2022-02-18 21:20:35 +00:00
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
2022-10-23 16:59:01 +00:00
request.toRequest()?.also {
2022-02-18 21:20:35 +00:00
if (requestCallBack(it)) destroyWebView()
2022-10-23 16:59:01 +00:00
}?.let(extraRequestList::add)
2022-02-18 21:20:35 +00:00
}
2022-01-16 15:35:06 +00:00
2022-02-18 21:20:35 +00:00
// Suppress image requests as we don't display them anywhere
// Less data, low chance of causing issues.
// blockNetworkImage also does this job but i will keep it for the future.
val blacklistedFiles = listOf(
".jpg",
".png",
".webp",
".mpg",
".mpeg",
".jpeg",
".webm",
".mp4",
".mp3",
".gifv",
".flv",
".asf",
".mov",
".mng",
".mkv",
".ogg",
".avi",
".wav",
".woff2",
".woff",
".ttf",
".css",
".vtt",
".srt",
".ts",
".gif",
// Warning, this might fuck some future sites, but it's used to make Sflix work.
"wss://"
)
/** NOTE! request.requestHeaders is not perfect!
* They don't contain all the headers the browser actually gives.
* Overriding with okhttp might fuck up otherwise working requests,
* e.g the recaptcha request.
* **/
return@runBlocking try {
when {
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
"/favicon.ico"
) -> WebResourceResponse(
"image/png",
null,
null
)
2022-08-21 18:14:26 +00:00
webViewUrl.contains("recaptcha") || webViewUrl.contains("/cdn-cgi/") -> super.shouldInterceptRequest(
2022-02-18 21:20:35 +00:00
view,
request
)
2022-08-21 18:14:26 +00:00
useOkhttp && request.method == "GET" -> app.get(
2022-02-18 21:20:35 +00:00
webViewUrl,
headers = request.requestHeaders
).okhttpResponse.toWebResourceResponse()
2022-02-18 21:20:35 +00:00
2022-08-21 18:14:26 +00:00
useOkhttp && request.method == "POST" -> app.post(
2022-02-18 21:20:35 +00:00
webViewUrl,
headers = request.requestHeaders
).okhttpResponse.toWebResourceResponse()
2022-08-21 18:14:26 +00:00
else -> super.shouldInterceptRequest(
2022-02-18 21:20:35 +00:00
view,
request
)
}
} catch (e: Exception) {
null
2021-10-18 16:04:05 +00:00
}
}
2022-02-18 21:20:35 +00:00
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.proceed() // Ignore ssl issues
}
}
2022-02-18 21:20:35 +00:00
webView?.loadUrl(url, headers.toMap())
} catch (e: Exception) {
logError(e)
}
}
var loop = 0
// Timeouts after this amount, 60s
2024-02-19 20:18:36 +00:00
val totalTime = timeout
val delayTime = 100L
// A bit sloppy, but couldn't find a better way
2022-08-21 18:38:53 +00:00
while (loop < totalTime / delayTime && !shouldExit) {
2022-01-16 15:35:06 +00:00
if (fixedRequest != null) return fixedRequest to extraRequestList
delay(delayTime)
loop += 1
}
println("Web-view timeout after ${totalTime / 1000}s")
destroyWebView()
2022-08-21 18:38:53 +00:00
return fixedRequest to extraRequestList
2022-01-16 15:35:06 +00:00
}
2022-08-21 18:14:26 +00:00
}
2022-01-16 15:35:06 +00:00
2022-10-23 16:59:01 +00:00
fun WebResourceRequest.toRequest(): Request? {
2022-08-21 18:14:26 +00:00
val webViewUrl = this.url.toString()
2022-10-23 16:59:01 +00:00
// If invalid url then it can crash with
// java.lang.IllegalArgumentException: Expected URL scheme 'http' or 'https' but was 'data'
// At Request.Builder().url(addParamsToUrl(url, params))
return normalSafeApiCall {
requestCreator(
this.method,
webViewUrl,
this.requestHeaders,
)
}
2022-08-21 18:14:26 +00:00
}
fun Response.toWebResourceResponse(): WebResourceResponse {
val contentTypeValue = this.header("Content-Type")
// 1. contentType. 2. charset
val typeRegex = Regex("""(.*);(?:.*charset=(.*)(?:|;)|)""")
return if (contentTypeValue != null) {
val found = typeRegex.find(contentTypeValue)
val contentType = found?.groupValues?.getOrNull(1)?.ifBlank { null } ?: contentTypeValue
val charset = found?.groupValues?.getOrNull(2)?.ifBlank { null }
WebResourceResponse(contentType, charset, this.body.byteStream())
} else {
WebResourceResponse("application/octet-stream", null, this.body.byteStream())
2021-10-18 16:04:05 +00:00
}
2022-08-21 18:14:26 +00:00
}