mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 18:47:03 +05:30
refactor(api): import CSRF extraction from android
module
This commit is contained in:
parent
ddfa62f4fb
commit
1cb3eb6472
9 changed files with 73 additions and 97 deletions
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
9
api/src/main/kotlin/dev/msfjarvis/claw/api/CSRFToken.kt
Normal file
9
api/src/main/kotlin/dev/msfjarvis/claw/api/CSRFToken.kt
Normal 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)
|
|
@ -29,6 +29,8 @@ interface LobstersApi {
|
||||||
@GET("~{username}.json")
|
@GET("~{username}.json")
|
||||||
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
|
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
|
||||||
|
|
||||||
|
@GET("/") suspend fun getCSRFToken(): ApiResult<CSRFToken, Unit>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BASE_URL = "https://lobste.rs"
|
const val BASE_URL = "https://lobste.rs"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import dagger.multibindings.IntKey
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
import dev.msfjarvis.claw.api.LobstersApi
|
import dev.msfjarvis.claw.api.LobstersApi
|
||||||
import dev.msfjarvis.claw.api.LobstersSearchApi
|
import dev.msfjarvis.claw.api.LobstersSearchApi
|
||||||
|
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||||
import dev.msfjarvis.claw.api.converters.SearchConverter
|
import dev.msfjarvis.claw.api.converters.SearchConverter
|
||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -81,6 +82,11 @@ object RetrofitModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory
|
fun provideApiResultCallAdapter(): CallAdapter.Factory = ApiResultCallAdapterFactory
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@IntKey(Int.MAX_VALUE)
|
||||||
|
@IntoMap
|
||||||
|
fun provideCSRFTokenConverter(): Converter.Factory = CSRFTokenConverter.Factory
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@SearchApi
|
@SearchApi
|
||||||
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
|
fun provideConverters(): List<@JvmSuppressWildcards Converter.Factory> =
|
||||||
|
|
|
@ -49,4 +49,14 @@ class ApiTest {
|
||||||
assertIs<Success<User>>(user)
|
assertIs<Success<User>>(user)
|
||||||
assertThat(user.value.username).isEqualTo("msfjarvis")
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ package dev.msfjarvis.claw.api
|
||||||
import com.slack.eithernet.ApiResult.Companion.success
|
import com.slack.eithernet.ApiResult.Companion.success
|
||||||
import com.slack.eithernet.test.EitherNetController
|
import com.slack.eithernet.test.EitherNetController
|
||||||
import com.slack.eithernet.test.enqueue
|
import com.slack.eithernet.test.enqueue
|
||||||
|
import dev.msfjarvis.claw.api.converters.CSRFTokenConverter
|
||||||
import dev.msfjarvis.claw.model.LobstersPost
|
import dev.msfjarvis.claw.model.LobstersPost
|
||||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||||
import dev.msfjarvis.claw.model.User
|
import dev.msfjarvis.claw.model.User
|
||||||
|
@ -16,6 +17,8 @@ import dev.msfjarvis.claw.util.TestUtils.getResource
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonNamingStrategy
|
import kotlinx.serialization.json.JsonNamingStrategy
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||||
|
@ -35,5 +38,12 @@ class ApiWrapper(controller: EitherNetController<LobstersApi>) {
|
||||||
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
|
controller.enqueue(LobstersApi::getHottestPosts) { success(hottest) }
|
||||||
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
|
controller.enqueue(LobstersApi::getPostDetails) { success(postDetails) }
|
||||||
controller.enqueue(LobstersApi::getUser) { success(user) }
|
controller.enqueue(LobstersApi::getUser) { success(user) }
|
||||||
|
controller.enqueue(LobstersApi::getCSRFToken) {
|
||||||
|
success(
|
||||||
|
CSRFTokenConverter.convert(
|
||||||
|
ResponseBody.create(MediaType.get("text/html"), getResource("search_chatgpt_page.html"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue