refactor(api): import CSRF extraction from android module

This commit is contained in:
Harsh Shandilya 2023-09-14 00:44:13 +05:30
parent ddfa62f4fb
commit 1cb3eb6472
No known key found for this signature in database
9 changed files with 73 additions and 97 deletions

View File

@ -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")
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -29,6 +29,8 @@ interface LobstersApi {
@GET("~{username}.json")
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
@GET("/") suspend fun getCSRFToken(): ApiResult<CSRFToken, Unit>
companion object {
const val BASE_URL = "https://lobste.rs"
}

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.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<ResponseBody, CSRFToken> {
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<out Annotation>,
retrofit: Retrofit,
): Converter<ResponseBody, CSRFToken> {
return CSRFTokenConverter
}
}
}

View File

@ -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> =

View File

@ -49,4 +49,14 @@ class ApiTest {
assertIs<Success<User>>(user)
assertThat(user.value.username).isEqualTo("msfjarvis")
}
@Test
fun `retrieve CSRF token`() = runTest {
val token = api.getCSRFToken()
assertIs<Success<CSRFToken>>(token)
assertThat(token.value.value)
.isEqualTo(
"oLI2VtS7LbkvxzGZQXgvl3E88RSwOw38Z_nlkxTk5r9JUznOv7sS8BeV_8h-jmI3aMJBh1mdRz4ckl8ItW3tlA"
)
}
}

View File

@ -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<LobstersApi>) {
@ -35,5 +38,12 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
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"))
)
)
}
}
}