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' jvmTarget = '1.8'
} }
buildFeatures { buildFeatures {
viewBinding true
compose true compose true
} }
composeOptions { composeOptions {
@ -163,6 +164,9 @@ dependencies {
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01" implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
implementation "androidx.datastore:datastore: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 // JDK Desugaring - polyfill for new Java APIs
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'

View File

@ -19,10 +19,10 @@
<meta-data <meta-data
android:name="io.sentry.auto-init" android:name="io.sentry.auto-init"
android:value="false" /> android:value="false" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Revolt"> android:theme="@style/Theme.Revolt">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -30,6 +30,9 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".activities.WebChallengeActivity"
android:theme="@style/Theme.Revolt" />
</application> </application>
</manifest> </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}" 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 private const val BACKEND_IS_STABLE = false
val RevoltJson = Json { ignoreUnknownKeys = true } val RevoltJson = Json { ignoreUnknownKeys = true }
@ -70,10 +74,7 @@ val RevoltHttp = HttpClient(OkHttp) {
defaultRequest { defaultRequest {
url(REVOLT_BASE) url(REVOLT_BASE)
header( header("User-Agent", buildUserAgent())
"User-Agent",
"Ktor RevoltAndroid/${BuildConfig.VERSION_NAME} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE})"
)
} }
} }

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