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 import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.mainWork 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 import java.net.URI /** * 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 * @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 * @param timeout close webview after timeout * */ class WebViewResolver( val interceptUrl: Regex, val additionalUrls: List = emptyList(), val userAgent: String? = USER_AGENT, val useOkhttp: Boolean = true, val script: String? = null, val scriptCallback: ((String) -> Unit)? = null, val timeout: Long = DEFAULT_TIMEOUT ) : Interceptor { constructor( interceptUrl: Regex, additionalUrls: List = 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 = emptyList(), userAgent: String? = USER_AGENT, useOkhttp: Boolean = true ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT) companion object { private const val DEFAULT_TIMEOUT = 60_000L 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 { val fixedRequest = resolveUsingWebView(request).first return@runBlocking chain.proceed(fixedRequest ?: request) } } suspend fun resolveUsingWebView( url: String, referer: String? = null, method: String = "GET", requestCallBack: (Request) -> Boolean = { false }, ): Pair> { 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() } } /** * @param requestCallBack asynchronously return matched requests by either interceptUrl or additionalUrls. If true, destroy WebView. * @return the final request (by interceptUrl) and all the collected urls (by additionalUrls). * */ @SuppressLint("SetJavaScriptEnabled") suspend fun resolveUsingWebView( request: Request, requestCallBack: (Request) -> Boolean = { false } ): Pair> { val url = request.url.toString() val headers = request.headers println("Initial web-view request: $url") var webView: WebView? = null // Extra assurance it exits as it should. var shouldExit = false fun destroyWebView() { main { webView?.stopLoading() webView?.destroy() webView = null shouldExit = true println("Destroyed webview") } } var fixedRequest: Request? = null val extraRequestList = threadSafeListOf() main { // Useful for debugging WebView.setWebContentsDebuggingEnabled(true) try { webView = WebView( AcraApplication.context ?: throw RuntimeException("No base context in WebViewResolver") ).apply { // Bare minimum to bypass captcha settings.javaScriptEnabled = true settings.domStorageEnabled = true webViewUserAgent = settings.userAgentString // Don't set user agent, setting user agent will make cloudflare break. if (userAgent != null) { settings.userAgentString = userAgent } // Blocks unnecessary images, remove if captcha fucks. // settings.blockNetworkImage = true } webView?.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? = runBlocking { val webViewUrl = request.url.toString() println("Loading WebView URL: $webViewUrl") if (script != null) { val handler = Handler(Looper.getMainLooper()) handler.post { view.evaluateJavascript("$script") { scriptCallback?.invoke(it) } } } if (interceptUrl.containsMatchIn(webViewUrl)) { fixedRequest = request.toRequest()?.also { requestCallBack(it) } println("Web-view request finished: $webViewUrl") destroyWebView() return@runBlocking null } if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) { request.toRequest()?.also { if (requestCallBack(it)) destroyWebView() }?.let(extraRequestList::add) } // 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 ) webViewUrl.contains("recaptcha") || webViewUrl.contains("/cdn-cgi/") -> super.shouldInterceptRequest( view, request ) useOkhttp && request.method == "GET" -> app.get( webViewUrl, headers = request.requestHeaders ).okhttpResponse.toWebResourceResponse() useOkhttp && request.method == "POST" -> app.post( webViewUrl, headers = request.requestHeaders ).okhttpResponse.toWebResourceResponse() else -> super.shouldInterceptRequest( view, request ) } } catch (e: Exception) { null } } override fun onReceivedSslError( view: WebView?, handler: SslErrorHandler?, error: SslError? ) { handler?.proceed() // Ignore ssl issues } } webView?.loadUrl(url, headers.toMap()) } catch (e: Exception) { logError(e) } } var loop = 0 // Timeouts after this amount, 60s val totalTime = timeout val delayTime = 100L // A bit sloppy, but couldn't find a better way while (loop < totalTime / delayTime && !shouldExit) { if (fixedRequest != null) return fixedRequest to extraRequestList delay(delayTime) loop += 1 } println("Web-view timeout after ${totalTime / 1000}s") destroyWebView() return fixedRequest to extraRequestList } } fun WebResourceRequest.toRequest(): Request? { val webViewUrl = this.url.toString() // 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, ) } } 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()) } }