1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 23:09:09 -05:00

Add webview to obtain cloudflare captcha cookies for okhttp (#2392)

This commit is contained in:
Mikołaj Pich 2024-01-12 18:34:43 +01:00 committed by GitHub
parent d8c4926a97
commit a98e8398fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 170 additions and 1 deletions

View File

@ -193,7 +193,7 @@ ext {
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.3.5'
implementation 'io.github.wulkanowy:sdk:2.3.6-SNAPSHOT'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -238,6 +238,7 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
implementation "com.jakewharton.timber:timber:5.0.1"
implementation 'com.github.Faierbel:slf4j-timber:2.0'

View File

@ -21,6 +21,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@ -43,6 +44,7 @@ internal class DataModule {
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(WebkitCookieManagerProxy())
// for debug only
addInterceptor(chuckerInterceptor, network = true)

View File

@ -11,6 +11,7 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
@ -77,6 +78,10 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
.show()
}
override fun onCaptchaVerificationRequired(url: String?) {
CaptchaDialog.newInstance(url).show(supportFragmentManager, "captcha_dialog")
}
override fun showDecryptionFailedDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired)

View File

@ -32,6 +32,10 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}

View File

@ -43,6 +43,10 @@ abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragme
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}

View File

@ -29,6 +29,7 @@ open class BasePresenter<T : BaseView>(
errorHandler.apply {
showErrorMessage = view::showError
onExpiredCredentials = view::showExpiredCredentialsDialog
onCaptchaVerificationRequired = view::onCaptchaVerificationRequired
onDecryptionFailed = view::showDecryptionFailedDialog
onNoCurrentStudent = view::openClearLoginView
onPasswordChangeRequired = view::showChangePasswordSnackbar

View File

@ -8,6 +8,8 @@ interface BaseView {
fun showExpiredCredentialsDialog()
fun onCaptchaVerificationRequired(url: String?)
fun showDecryptionFailedDialog()
fun showAuthDialog()

View File

@ -4,6 +4,7 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import io.github.wulkanowy.utils.getErrorString
@ -25,6 +26,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var onAuthorizationRequired: () -> Unit = {}
var onCaptchaVerificationRequired: (url: String?) -> Unit = {}
fun dispatch(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error)
@ -38,6 +41,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}

View File

@ -0,0 +1,72 @@
package io.github.wulkanowy.ui.modules.captcha
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.databinding.DialogCaptchaBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseDialogFragment
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
@Inject
lateinit var sdk: Sdk
companion object {
private const val CAPTCHA_URL = "captcha_url"
fun newInstance(url: String?): CaptchaDialog {
return CaptchaDialog().apply {
arguments = bundleOf(CAPTCHA_URL to url)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = DialogCaptchaBinding.inflate(inflater).apply { binding = this }.root
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding.captchaWebview) {
with(settings) {
javaScriptEnabled = true
userAgentString = sdk.userAgent
}
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.evaluateJavascript("document.getElementById('challenge-running') == undefined") {
if (it == "true") {
dismiss()
} else Timber.e("JS result: $it")
}
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
}
}
loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty())
}
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.main.MainView
import timber.log.Timber
@ -26,6 +27,8 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin
override fun showExpiredCredentialsDialog() {}
override fun onCaptchaVerificationRequired(url: String?) = Unit
override fun showDecryptionFailedDialog() {}
override fun openClearLoginView() {}

View File

@ -51,6 +51,10 @@ class AdvancedFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}

View File

@ -67,6 +67,10 @@ class AppearanceFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}

View File

@ -137,6 +137,10 @@ class NotificationsFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}

View File

@ -88,6 +88,10 @@ class SyncFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}

View File

@ -0,0 +1,39 @@
package io.github.wulkanowy.utils
import java.net.CookiePolicy
import java.net.URI
import android.webkit.CookieManager as WebkitCookieManager
import java.net.CookieManager as JavaCookieManager
class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
private val webkitCookieManager: WebkitCookieManager = WebkitCookieManager.getInstance()
override fun put(uri: URI?, responseHeaders: Map<String?, List<String?>>?) {
if (uri == null || responseHeaders == null) return
val url = uri.toString()
for (headerKey in responseHeaders.keys) {
if (headerKey == null || !(
headerKey.equals("Set-Cookie2", ignoreCase = true) ||
headerKey.equals("Set-Cookie", ignoreCase = true)
)
) continue
// process each of the headers
for (headerValue in responseHeaders[headerKey].orEmpty()) {
webkitCookieManager.setCookie(url, headerValue)
}
}
}
override operator fun get(
uri: URI?,
requestHeaders: Map<String?, List<String?>?>?
): Map<String, List<String>> {
require(!(uri == null || requestHeaders == null)) { "Argument is null" }
val res = mutableMapOf<String, List<String>>()
val cookie = webkitCookieManager.getCookie(uri.toString())
if (cookie != null) res["Cookie"] = listOf(cookie)
return res
}
}

View File

@ -0,0 +1,12 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.captcha.CaptchaDialog">
<WebView
android:id="@+id/captcha_webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -105,6 +105,10 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}