mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Add cloudflare bypass
This commit is contained in:
		
							parent
							
								
									54d4193734
								
							
						
					
					
						commit
						66a99a2624
					
				
					 7 changed files with 165 additions and 42 deletions
				
			
		|  | @ -0,0 +1,94 @@ | |||
| package com.lagradost.cloudstream3.network | ||||
| 
 | ||||
| import android.util.Log | ||||
| import android.webkit.CookieManager | ||||
| import androidx.annotation.AnyThread | ||||
| import com.lagradost.cloudstream3.app | ||||
| import com.lagradost.cloudstream3.mvvm.debugWarning | ||||
| import com.lagradost.nicehttp.Requests.Companion.await | ||||
| import com.lagradost.nicehttp.cookies | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import okhttp3.* | ||||
| 
 | ||||
| 
 | ||||
| @AnyThread | ||||
| class CloudflareKiller : Interceptor { | ||||
|     companion object { | ||||
|         const val TAG = "CloudflareKiller" | ||||
|         fun parseCookieMap(cookie: String): Map<String, String> { | ||||
|             return cookie.split(";").associate { | ||||
|                 val split = it.split("=") | ||||
|                 (split.getOrNull(0)?.trim() ?: "") to (split.getOrNull(1)?.trim() ?: "") | ||||
|             }.filter { it.key.isNotBlank() && it.value.isNotBlank() } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     val savedCookies: MutableMap<String, String> = mutableMapOf() | ||||
| 
 | ||||
|     override fun intercept(chain: Interceptor.Chain): Response = runBlocking { | ||||
|         val request = chain.request() | ||||
|         if (savedCookies[request.url.host] == null) { | ||||
|             bypassCloudflare(request).also { | ||||
|                 debugWarning({ it == null }) { "Failed cloudflare at: ${request.url}" } | ||||
|             }?.let { | ||||
|                 return@runBlocking it | ||||
|             } | ||||
|         } | ||||
|         return@runBlocking chain.proceed(request) | ||||
|     } | ||||
| 
 | ||||
|     private fun getWebViewCookie(url: String): String? { | ||||
|         return CookieManager.getInstance()?.getCookie(url) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns true if the cf cookies were successfully fetched from the CookieManager | ||||
|      * Also saves the cookies. | ||||
|      * */ | ||||
|     private fun trySolveWithSavedCookies(request: Request): Boolean { | ||||
|         // Not sure if this takes expiration into account | ||||
|         return getWebViewCookie(request.url.toString())?.let { cookie -> | ||||
|             cookie.contains("cf_clearance").also { solved -> | ||||
|                 if (solved) savedCookies[request.url.host] = cookie | ||||
|             } | ||||
|         } ?: false | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun bypassCloudflare(request: Request): Response? { | ||||
|         val url = request.url.toString() | ||||
| 
 | ||||
|         // If no cookies then try to get them | ||||
|         // Remove this if statement if cookies expire | ||||
|         if (!trySolveWithSavedCookies(request)) { | ||||
|             Log.d(TAG, "Loading webview to solve cloudflare for ${request.url}") | ||||
|             WebViewResolver( | ||||
|                 // Never exit based on url | ||||
|                 Regex(".^"), | ||||
|                 // Cloudflare needs default user agent | ||||
|                 userAgent = null, | ||||
|                 // Cannot use okhttp (i think intercepting cookies fails which causes the issues) | ||||
|                 useOkhttp = false, | ||||
|                 // Match every url for the requestCallBack | ||||
|                 additionalUrls = listOf(Regex(".")) | ||||
|             ).resolveUsingWebView( | ||||
|                 url | ||||
|             ) { | ||||
|                 trySolveWithSavedCookies(request) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val cookies = savedCookies[request.url.host] ?: return null | ||||
| 
 | ||||
|         val mappedCookies = parseCookieMap(cookies) | ||||
|         val userAgentMap = WebViewResolver.getWebViewUserAgent()?.let { | ||||
|             mapOf("user-agent" to it) | ||||
|         } ?: emptyMap() | ||||
| 
 | ||||
|         val headers = getHeaders(request.headers.toMap() + userAgentMap, mappedCookies + request.cookies) | ||||
|         return app.baseClient.newCall( | ||||
|             request.newBuilder() | ||||
|                 .headers(headers) | ||||
|                 .build() | ||||
|         ).await() | ||||
|     } | ||||
| } | ||||
|  | @ -1,13 +1,10 @@ | |||
| package com.lagradost.cloudstream3.network | ||||
| 
 | ||||
| import androidx.annotation.AnyThread | ||||
| import com.lagradost.cloudstream3.USER_AGENT | ||||
| import com.lagradost.cloudstream3.app | ||||
| import com.lagradost.nicehttp.Requests.Companion.await | ||||
| import com.lagradost.nicehttp.getCookies | ||||
| import com.lagradost.nicehttp.cookies | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import okhttp3.Headers | ||||
| import okhttp3.Headers.Companion.toHeaders | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
|  |  | |||
|  | @ -43,10 +43,10 @@ fun Requests.initClient(context: Context): OkHttpClient { | |||
|     return baseClient | ||||
| } | ||||
| 
 | ||||
| val Request.cookies: Map<String, String> | ||||
|     get() { | ||||
|         return this.headers.getCookies("Cookie") | ||||
|     } | ||||
| //val Request.cookies: Map<String, String> | ||||
| //    get() { | ||||
| //        return this.headers.getCookies("Cookie") | ||||
| //    } | ||||
| 
 | ||||
| private val DEFAULT_HEADERS = mapOf("user-agent" to USER_AGENT) | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,10 +4,12 @@ import android.annotation.SuppressLint | |||
| import android.net.http.SslError | ||||
| 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.logError | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.main | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.mainWork | ||||
| import com.lagradost.nicehttp.requestCreator | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.runBlocking | ||||
|  | @ -21,14 +23,33 @@ import java.net.URI | |||
|  * @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. | ||||
|  * */ | ||||
| class WebViewResolver( | ||||
|     val interceptUrl: Regex, | ||||
|     val additionalUrls: List<Regex> = emptyList(), | ||||
|     val userAgent: String? = USER_AGENT, | ||||
|     val useOkhttp: Boolean = true | ||||
| ) : | ||||
|     Interceptor { | ||||
| 
 | ||||
|     companion object { | ||||
|         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 { | ||||
|  | @ -76,7 +97,7 @@ class WebViewResolver( | |||
| 
 | ||||
|         main { | ||||
|             // Useful for debugging | ||||
| //            WebView.setWebContentsDebuggingEnabled(true) | ||||
|             WebView.setWebContentsDebuggingEnabled(true) | ||||
|             try { | ||||
|                 webView = WebView( | ||||
|                     AcraApplication.context | ||||
|  | @ -86,12 +107,13 @@ class WebViewResolver( | |||
|                     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 | ||||
| //                    settings.blockNetworkImage = true | ||||
|                 } | ||||
| 
 | ||||
|                 webView?.webViewClient = object : WebViewClient() { | ||||
|  | @ -100,11 +122,11 @@ class WebViewResolver( | |||
|                         request: WebResourceRequest | ||||
|                     ): WebResourceResponse? = runBlocking { | ||||
|                         val webViewUrl = request.url.toString() | ||||
| //                    println("Loading WebView URL: $webViewUrl") | ||||
|                         println("Loading WebView URL: $webViewUrl") | ||||
| 
 | ||||
|                         if (interceptUrl.containsMatchIn(webViewUrl)) { | ||||
|                             fixedRequest = request.toRequest().also { | ||||
|                                 if (requestCallBack(it)) destroyWebView() | ||||
|                                 requestCallBack(it) | ||||
|                             } | ||||
|                             println("Web-view request finished: $webViewUrl") | ||||
|                             destroyWebView() | ||||
|  | @ -166,22 +188,22 @@ class WebViewResolver( | |||
|                                     null, | ||||
|                                     null | ||||
|                                 ) | ||||
| 
 | ||||
|                                 webViewUrl.contains("recaptcha") -> super.shouldInterceptRequest( | ||||
|                                 webViewUrl.contains("recaptcha") || webViewUrl.contains("/cdn-cgi/") -> super.shouldInterceptRequest( | ||||
|                                     view, | ||||
|                                     request | ||||
|                                 ) | ||||
| 
 | ||||
|                                 request.method == "GET" -> app.get( | ||||
|                                 useOkhttp && request.method == "GET" -> app.get( | ||||
|                                     webViewUrl, | ||||
|                                     headers = request.requestHeaders | ||||
|                                 ).okhttpResponse.toWebResourceResponse() | ||||
| 
 | ||||
|                                 request.method == "POST" -> app.post( | ||||
|                                 useOkhttp && request.method == "POST" -> app.post( | ||||
|                                     webViewUrl, | ||||
|                                     headers = request.requestHeaders | ||||
|                                 ).okhttpResponse.toWebResourceResponse() | ||||
|                                 else -> return@runBlocking super.shouldInterceptRequest( | ||||
| 
 | ||||
|                                 else -> super.shouldInterceptRequest( | ||||
|                                     view, | ||||
|                                     request | ||||
|                                 ) | ||||
|  | @ -223,27 +245,28 @@ class WebViewResolver( | |||
|         return null to extraRequestList | ||||
|     } | ||||
| 
 | ||||
|     fun WebResourceRequest.toRequest(): Request { | ||||
|         val webViewUrl = this.url.toString() | ||||
| } | ||||
| 
 | ||||
|         return requestCreator( | ||||
|             this.method, | ||||
|             webViewUrl, | ||||
|             this.requestHeaders, | ||||
|         ) | ||||
|     } | ||||
| fun WebResourceRequest.toRequest(): Request { | ||||
|     val webViewUrl = this.url.toString() | ||||
| 
 | ||||
|     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()) | ||||
|         } | ||||
|     return 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()) | ||||
|     } | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,16 @@ | |||
| package com.lagradost.cloudstream3.ui | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import androidx.fragment.app.Fragment | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.webkit.WebResourceRequest | ||||
| import android.webkit.WebView | ||||
| import android.webkit.WebViewClient | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.navigation.fragment.findNavController | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.USER_AGENT | ||||
| import com.lagradost.cloudstream3.network.WebViewResolver | ||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.loadRepository | ||||
| import kotlinx.android.synthetic.main.fragment_webview.* | ||||
|  | @ -48,7 +48,9 @@ class WebviewFragment : Fragment() { | |||
|         } | ||||
|         web_view.settings.javaScriptEnabled = true | ||||
|         web_view.settings.domStorageEnabled = true | ||||
|         web_view.settings.userAgentString = USER_AGENT | ||||
| 
 | ||||
|         WebViewResolver.webViewUserAgent = web_view.settings.userAgentString | ||||
| //        web_view.settings.userAgentString = USER_AGENT | ||||
|         web_view.loadUrl(url) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -288,7 +288,7 @@ object AppUtils { | |||
|         return this.packageManager.hasSystemFeature("android.software.webview") | ||||
|     } | ||||
| 
 | ||||
|     private fun openWebView(fragment: Fragment?, url: String) { | ||||
|     fun openWebView(fragment: Fragment?, url: String) { | ||||
|         if (fragment?.context?.hasWebView() == true) | ||||
|             normalSafeApiCall { | ||||
|                 fragment | ||||
|  |  | |||
|  | @ -43,6 +43,13 @@ object Coroutines { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     suspend fun <T, V> V.mainWork(work: suspend (CoroutineScope.(V) -> T)): T { | ||||
|         val value = this | ||||
|         return withContext(Dispatchers.Main) { | ||||
|             work(value) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun runOnMainThread(work: (() -> Unit)) { | ||||
|         val mainHandler = Handler(Looper.getMainLooper()) | ||||
|         mainHandler.post { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue