2021-10-04 17:54:15 +00:00
|
|
|
package com.lagradost.cloudstream3.network
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
2021-10-07 17:40:31 +00:00
|
|
|
import android.net.http.SslError
|
2024-01-19 19:38:37 +00:00
|
|
|
import android.os.Handler
|
|
|
|
import android.os.Looper
|
2021-10-07 17:40:31 +00:00
|
|
|
import android.webkit.*
|
2021-10-04 17:54:15 +00:00
|
|
|
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
|
2021-12-05 16:22:30 +00:00
|
|
|
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
|
2021-10-04 17:54:15 +00:00
|
|
|
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
|
2022-05-02 16:58:27 +00:00
|
|
|
import com.lagradost.nicehttp.requestCreator
|
2021-10-04 17:54:15 +00:00
|
|
|
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.
|
2022-08-21 15:50:12 +00:00
|
|
|
* @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.
|
2024-01-19 19:38:37 +00:00
|
|
|
* @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
|
|
|
* */
|
2022-08-21 15:50:12 +00:00
|
|
|
class WebViewResolver(
|
|
|
|
val interceptUrl: Regex,
|
|
|
|
val additionalUrls: List<Regex> = emptyList(),
|
|
|
|
val userAgent: String? = USER_AGENT,
|
2024-01-19 19:38:37 +00:00
|
|
|
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-08-21 15:50:12 +00:00
|
|
|
) :
|
2022-01-16 15:35:06 +00:00
|
|
|
Interceptor {
|
2021-10-04 17:54:15 +00:00
|
|
|
|
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)
|
|
|
|
|
2024-01-19 19:38:37 +00:00
|
|
|
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)
|
2024-01-19 19:38:37 +00:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 17:54:15 +00:00
|
|
|
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
|
2021-10-04 17:54:15 +00:00
|
|
|
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 },
|
2022-08-21 15:50:12 +00:00
|
|
|
): 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).
|
|
|
|
* */
|
2021-10-04 17:54:15 +00:00
|
|
|
@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>> {
|
2021-10-04 17:54:15 +00:00
|
|
|
val url = request.url.toString()
|
2021-10-10 20:01:02 +00:00
|
|
|
val headers = request.headers
|
2021-10-07 17:40:31 +00:00
|
|
|
println("Initial web-view request: $url")
|
2021-10-04 17:54:15 +00:00
|
|
|
var webView: WebView? = null
|
2022-08-21 18:38:53 +00:00
|
|
|
// Extra assurance it exits as it should.
|
|
|
|
var shouldExit = false
|
2021-10-04 17:54:15 +00:00
|
|
|
|
|
|
|
fun destroyWebView() {
|
|
|
|
main {
|
|
|
|
webView?.stopLoading()
|
|
|
|
webView?.destroy()
|
|
|
|
webView = null
|
2022-08-21 18:38:53 +00:00
|
|
|
shouldExit = true
|
2021-10-04 17:54:15 +00:00
|
|
|
println("Destroyed webview")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var fixedRequest: Request? = null
|
2022-10-18 22:14:15 +00:00
|
|
|
val extraRequestList = threadSafeListOf<Request>()
|
2021-10-04 17:54:15 +00:00
|
|
|
|
|
|
|
main {
|
2021-10-09 12:19:26 +00:00
|
|
|
// 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 15:50:12 +00:00
|
|
|
|
2022-08-21 18:14:26 +00:00
|
|
|
webViewUserAgent = settings.userAgentString
|
2022-08-21 15:50:12 +00:00
|
|
|
// 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
|
|
|
}
|
2021-10-04 17:54:15 +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
|
|
|
|
2024-01-19 19:38:37 +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
|
2022-05-02 16:58:27 +00:00
|
|
|
).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
|
2022-05-02 16:58:27 +00:00
|
|
|
).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
|
|
|
}
|
|
|
|
}
|
2021-10-07 17:40:31 +00:00
|
|
|
|
2022-02-18 21:20:35 +00:00
|
|
|
override fun onReceivedSslError(
|
|
|
|
view: WebView?,
|
|
|
|
handler: SslErrorHandler?,
|
|
|
|
error: SslError?
|
|
|
|
) {
|
|
|
|
handler?.proceed() // Ignore ssl issues
|
|
|
|
}
|
2021-10-07 17:40:31 +00:00
|
|
|
}
|
2022-02-18 21:20:35 +00:00
|
|
|
webView?.loadUrl(url, headers.toMap())
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
2021-10-04 17:54:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var loop = 0
|
2021-10-09 12:19:26 +00:00
|
|
|
// Timeouts after this amount, 60s
|
2024-02-19 20:18:36 +00:00
|
|
|
val totalTime = timeout
|
2021-10-09 12:19:26 +00:00
|
|
|
|
2021-10-04 17:54:15 +00:00
|
|
|
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
|
2021-10-04 17:54:15 +00:00
|
|
|
delay(delayTime)
|
|
|
|
loop += 1
|
|
|
|
}
|
|
|
|
|
2021-10-07 17:40:31 +00:00
|
|
|
println("Web-view timeout after ${totalTime / 1000}s")
|
2021-10-04 17:54:15 +00:00
|
|
|
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()
|
2021-10-04 17:54:15 +00:00
|
|
|
|
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
|
|
|
}
|