From 1cb3eb64727e764979fda2b46594ea2544a127bc Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 14 Sep 2023 00:44:13 +0530 Subject: [PATCH] refactor(api): import CSRF extraction from `android` module --- .../claw/android/viewmodel/CSRFRepository.kt | 36 ----------- .../android/viewmodel/CSRFRepositoryTest.kt | 61 ------------------- .../dev/msfjarvis/claw/api/CSRFToken.kt | 9 +++ .../dev/msfjarvis/claw/api/LobstersApi.kt | 2 + .../claw/api/converters/CSRFTokenConverter.kt | 36 +++++++++++ .../claw/api/injection/RetrofitModule.kt | 6 ++ .../kotlin/dev/msfjarvis/claw/api/ApiTest.kt | 10 +++ .../dev/msfjarvis/claw/api/ApiWrapper.kt | 10 +++ .../src/test/resources/csrf_page.html | 0 9 files changed, 73 insertions(+), 97 deletions(-) delete mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepository.kt delete mode 100644 android/src/test/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepositoryTest.kt create mode 100644 api/src/main/kotlin/dev/msfjarvis/claw/api/CSRFToken.kt create mode 100644 api/src/main/kotlin/dev/msfjarvis/claw/api/converters/CSRFTokenConverter.kt rename {android => api}/src/test/resources/csrf_page.html (100%) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepository.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepository.kt deleted file mode 100644 index 9800f01e..00000000 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepository.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.api.injection.BaseUrl -import dev.msfjarvis.claw.core.injection.IODispatcher -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).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") - } - } - } -} diff --git a/android/src/test/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepositoryTest.kt b/android/src/test/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepositoryTest.kt deleted file mode 100644 index 32d03891..00000000 --- a/android/src/test/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepositoryTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.runTest -import okhttp3.OkHttpClient -import okhttp3.mockwebserver.Dispatcher -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test - -class CSRFRepositoryTest { - @Test - fun `correctly extracts CSRF token`() = runTest { - val repo = - CSRFRepository( - OkHttpClient.Builder().build(), - Dispatchers.Default, - server.url("/").toString(), - ) - assertThat(repo.extractToken()) - .isEqualTo( - "OZWykgFemPVeOSNmB53-ccKXe458X7xCInO1-qzFU6nk_9RCSrSQqS9OPmA5_pyy8qD3IYAIZ7XfAM3gdhJpkQ" - ) - } - - companion object { - private val server = MockWebServer() - - @JvmStatic - @BeforeAll - fun setup() { - 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 - } - } -} diff --git a/api/src/main/kotlin/dev/msfjarvis/claw/api/CSRFToken.kt b/api/src/main/kotlin/dev/msfjarvis/claw/api/CSRFToken.kt new file mode 100644 index 00000000..a2bbe306 --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/claw/api/CSRFToken.kt @@ -0,0 +1,9 @@ +/* + * 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. + */ +package dev.msfjarvis.claw.api + +@JvmInline value class CSRFToken(val value: String) diff --git a/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt b/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt index eaeb137a..f4aedc09 100644 --- a/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt +++ b/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt @@ -29,6 +29,8 @@ interface LobstersApi { @GET("~{username}.json") suspend fun getUser(@Path("username") username: String): ApiResult + @GET("/") suspend fun getCSRFToken(): ApiResult + companion object { const val BASE_URL = "https://lobste.rs" } diff --git a/api/src/main/kotlin/dev/msfjarvis/claw/api/converters/CSRFTokenConverter.kt b/api/src/main/kotlin/dev/msfjarvis/claw/api/converters/CSRFTokenConverter.kt new file mode 100644 index 00000000..18bf8695 --- /dev/null +++ b/api/src/main/kotlin/dev/msfjarvis/claw/api/converters/CSRFTokenConverter.kt @@ -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.api.converters + +import dev.msfjarvis.claw.api.CSRFToken +import dev.msfjarvis.claw.api.LobstersApi +import java.lang.reflect.Type +import okhttp3.ResponseBody +import org.jsoup.Jsoup +import retrofit2.Converter +import retrofit2.Retrofit + +object CSRFTokenConverter : Converter { + override fun convert(value: ResponseBody): CSRFToken { + val token = + Jsoup.parse(value.string(), LobstersApi.BASE_URL) + .select("meta[name=\"csrf-token\"]") + .first()!! + .attr("content") + return CSRFToken(token) + } + + object Factory : Converter.Factory() { + override fun responseBodyConverter( + type: Type, + annotations: Array, + retrofit: Retrofit, + ): Converter { + return CSRFTokenConverter + } + } +} diff --git a/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/RetrofitModule.kt b/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/RetrofitModule.kt index 8fa103e7..9146b911 100644 --- a/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/RetrofitModule.kt +++ b/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/RetrofitModule.kt @@ -16,6 +16,7 @@ import dagger.multibindings.IntKey import dagger.multibindings.IntoMap import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.api.LobstersSearchApi +import dev.msfjarvis.claw.api.converters.CSRFTokenConverter import dev.msfjarvis.claw.api.converters.SearchConverter import javax.inject.Qualifier import okhttp3.OkHttpClient @@ -81,6 +82,11 @@ object RetrofitModule { @IntoMap fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory + @Provides + @IntKey(Int.MAX_VALUE) + @IntoMap + fun provideCSRFTokenConverter(): Converter.Factory = CSRFTokenConverter.Factory + @Provides @SearchApi fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> = diff --git a/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiTest.kt b/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiTest.kt index 94ba1752..0d134a6b 100644 --- a/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiTest.kt +++ b/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiTest.kt @@ -49,4 +49,14 @@ class ApiTest { assertIs>(user) assertThat(user.value.username).isEqualTo("msfjarvis") } + + @Test + fun `retrieve CSRF token`() = runTest { + val token = api.getCSRFToken() + assertIs>(token) + assertThat(token.value.value) + .isEqualTo( + "oLI2VtS7LbkvxzGZQXgvl3E88RSwOw38Z_nlkxTk5r9JUznOv7sS8BeV_8h-jmI3aMJBh1mdRz4ckl8ItW3tlA" + ) + } } diff --git a/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiWrapper.kt b/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiWrapper.kt index f58f20c1..e153c338 100644 --- a/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiWrapper.kt +++ b/api/src/test/kotlin/dev/msfjarvis/claw/api/ApiWrapper.kt @@ -9,6 +9,7 @@ package dev.msfjarvis.claw.api import com.slack.eithernet.ApiResult.Companion.success import com.slack.eithernet.test.EitherNetController import com.slack.eithernet.test.enqueue +import dev.msfjarvis.claw.api.converters.CSRFTokenConverter import dev.msfjarvis.claw.model.LobstersPost import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.User @@ -16,6 +17,8 @@ import dev.msfjarvis.claw.util.TestUtils.getResource import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNamingStrategy +import okhttp3.MediaType +import okhttp3.ResponseBody @OptIn(ExperimentalSerializationApi::class) class ApiWrapper(controller: EitherNetController) { @@ -35,5 +38,12 @@ class ApiWrapper(controller: EitherNetController) { controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) } controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) } controller.enqueue(LobstersApi::getUser) { success(user) } + controller.enqueue(LobstersApi::getCSRFToken) { + success( + CSRFTokenConverter.convert( + ResponseBody.create(MediaType.get("text/html"), getResource("search_chatgpt_page.html")) + ) + ) + } } } diff --git a/android/src/test/resources/csrf_page.html b/api/src/test/resources/csrf_page.html similarity index 100% rename from android/src/test/resources/csrf_page.html rename to api/src/test/resources/csrf_page.html