feat: handle cloudflare captcha in under attack mode

This commit is contained in:
Infi 2023-04-01 00:09:18 +02:00
parent 6f48f65bd1
commit 0adb40f661
7 changed files with 111 additions and 5 deletions

View File

@ -83,6 +83,7 @@ android {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
compose true
}
composeOptions {
@ -163,6 +164,9 @@ dependencies {
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
implementation "androidx.datastore:datastore:1.1.0-alpha01"
// Libraries used for legacy View-based UI
implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha09"
// JDK Desugaring - polyfill for new Java APIs
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'

View File

@ -19,10 +19,10 @@
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Revolt">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -30,6 +30,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.WebChallengeActivity"
android:theme="@style/Theme.Revolt" />
</application>
</manifest>

View File

@ -0,0 +1,47 @@
package chat.revolt.activities
import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import chat.revolt.api.REVOLT_BASE
import chat.revolt.api.buildUserAgent
import chat.revolt.databinding.ActivityWebchallengeBinding
private class WebChallengeClient(val pageLoaded: () -> Unit) : WebViewClient() {
@Override
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
pageLoaded()
}
}
class WebChallengeActivity : AppCompatActivity() {
private lateinit var binding: ActivityWebchallengeBinding
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWebchallengeBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
userAgentString = buildUserAgent("WebChallenge")
}
binding.webView.webViewClient = WebChallengeClient {
binding.webView.evaluateJavascript(
"(function() { return document.getElementById('cf-wrapper') != null; })();"
) { result ->
if (result == "false") { // No challenge
finish()
}
}
}
binding.webView.loadUrl(REVOLT_BASE)
}
}

View File

@ -41,6 +41,10 @@ fun asJanuaryProxyUrl(url: String): String {
return "$REVOLT_JANUARY/proxy?url=${url}"
}
fun buildUserAgent(accessMethod: String = "Ktor"): String {
return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE})"
}
private const val BACKEND_IS_STABLE = false
val RevoltJson = Json { ignoreUnknownKeys = true }
@ -70,10 +74,7 @@ val RevoltHttp = HttpClient(OkHttp) {
defaultRequest {
url(REVOLT_BASE)
header(
"User-Agent",
"Ktor RevoltAndroid/${BuildConfig.VERSION_NAME} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE})"
)
header("User-Agent", buildUserAgent())
}
}

View File

@ -0,0 +1,15 @@
package chat.revolt.api.internals
import chat.revolt.api.REVOLT_BASE
import chat.revolt.api.RevoltHttp
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
object WebChallenge {
suspend fun needsCloudflare(): Boolean {
RevoltHttp.get(REVOLT_BASE).let {
val text = it.bodyAsText()
return text.contains("window._cf_chl_opt") // FIXME Naive, prone to captcha page changing
}
}
}

View File

@ -2,8 +2,10 @@ package chat.revolt.screens
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -11,6 +13,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@ -18,7 +21,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.activities.WebChallengeActivity
import chat.revolt.api.RevoltAPI
import chat.revolt.api.internals.WebChallenge
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.screens.splash.DisconnectedScreen
@ -70,11 +75,19 @@ class SplashScreenViewModel @Inject constructor(
}
fun checkLoggedInState() {
Log.d("SplashScreenViewModel", "Checking logged in state")
viewModelScope.launch {
setIsConnected(hasInternetConnection())
if (!isConnected) return@launch
val needsCloudflare = WebChallenge.needsCloudflare()
if (needsCloudflare) {
setNavigateTo("webchallenge")
return@launch
}
val token = kvStorage.get("sessionToken") ?: return@launch setNavigateTo("login")
val valid = RevoltAPI.checkSessionToken(token)
@ -107,6 +120,8 @@ fun SplashScreen(
navController: NavController,
viewModel: SplashScreenViewModel = hiltViewModel()
) {
val context = LocalContext.current
if (!viewModel.isConnected) {
DisconnectedScreen(
onRetry = {
@ -143,6 +158,17 @@ fun SplashScreen(
}
}
}
"webchallenge" -> {
context.startActivity(
Intent(
context,
WebChallengeActivity::class.java
)
)
viewModel.checkLoggedInState()
}
"home" -> {
navController.navigate("chat") {
popUpTo("splash") {

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webView" />
</androidx.constraintlayout.widget.ConstraintLayout>