feat(android): add a repository for extracting CSRF token

This commit is contained in:
Harsh Shandilya 2023-03-10 01:28:53 +05:30
parent 93d2397572
commit c1f1d67bfa
No known key found for this signature in database
7 changed files with 111 additions and 3 deletions

View file

@ -66,7 +66,11 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.coil)
implementation(libs.copydown)
implementation(libs.jsoup)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.sqldelight.extensions.coroutines)
testImplementation(libs.kotest.assertions.core)
testImplementation(libs.kotest.runner.junit5)
testImplementation(libs.okhttp.mockwebserver)
}

View file

@ -0,0 +1,36 @@
/*
* Copyright © 2023 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.viewmodel
import dev.msfjarvis.claw.android.injection.IODispatcher
import dev.msfjarvis.claw.api.injection.BaseUrl
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
/** Helper for extracting CSRF token for authenticated requests to https://lobste.rs. */
class CSRFRepository
@Inject
constructor(
private val okHttpClient: OkHttpClient,
@IODispatcher private val dispatcher: CoroutineDispatcher,
@BaseUrl private val url: String,
) {
suspend fun extractToken(): String? {
val request = Request.Builder().url(url).get().build()
return withContext(dispatcher) {
okHttpClient.newCall(request).execute().use { response ->
val doc = Jsoup.parse(response.body?.string() ?: return@use null)
val element = doc.select("meta[name=\"csrf-token\"]").first() ?: return@use null
return@use element.attr("content")
}
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright © 2023 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.android.viewmodel
import io.kotest.core.spec.Spec
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.Dispatchers
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
class CSRFRepositoryTest : FunSpec() {
private val server = MockWebServer()
init {
test("Correctly extracts CSRF token").config(coroutineTestScope = true) {
val repo =
CSRFRepository(
OkHttpClient.Builder().build(),
Dispatchers.Default,
server.url("/").toString(),
)
repo.extractToken() shouldBe
"OZWykgFemPVeOSNmB53-ccKXe458X7xCInO1-qzFU6nk_9RCSrSQqS9OPmA5_pyy8qD3IYAIZ7XfAM3gdhJpkQ"
}
}
override suspend fun beforeSpec(spec: Spec) {
super.beforeSpec(spec)
val dispatcher =
object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
return when (val path = request.path) {
"/" ->
MockResponse()
.setResponseCode(200)
.setBody(
javaClass.classLoader!!
.getResourceAsStream("csrf_page.html")
.readAllBytes()
.decodeToString(),
)
else -> error("Invalid path: $path")
}
}
}
server.dispatcher = dispatcher
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Copyright © 2021-2023 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
@ -13,6 +13,7 @@ import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dev.msfjarvis.claw.api.LobstersApi
import javax.inject.Qualifier
import okhttp3.OkHttpClient
import retrofit2.Converter
import retrofit2.Retrofit
@ -25,10 +26,11 @@ object ApiModule {
fun provideRetrofit(
client: OkHttpClient,
converterFactories: Set<@JvmSuppressWildcards Converter.Factory>,
@BaseUrl baseUrl: String,
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(LobstersApi.BASE_URL)
.baseUrl(baseUrl)
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.apply { converterFactories.forEach(this::addConverterFactory) }
@ -39,4 +41,8 @@ object ApiModule {
fun provideApi(retrofit: Retrofit): LobstersApi {
return retrofit.create()
}
@Provides @BaseUrl fun provideBaseUrl(): String = LobstersApi.BASE_URL
}
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrl

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Copyright © 2022-2023 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.

View file

@ -77,6 +77,7 @@ napier = "io.github.aakira:napier:2.6.1"
okhttp-bom = "com.squareup.okhttp3:okhttp-bom:4.10.0"
okhttp-core = { module = "com.squareup.okhttp3:okhttp" }
okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-kotlinxSerializationConverter = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry-sdk" }