diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 569127a2..e3baf329 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -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) } 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 new file mode 100644 index 00000000..147bc11a --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepository.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.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") + } + } + } +} 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 new file mode 100644 index 00000000..ca9428ff --- /dev/null +++ b/android/src/test/kotlin/dev/msfjarvis/claw/android/viewmodel/CSRFRepositoryTest.kt @@ -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 + } +} diff --git a/android/src/test/resources/csrf_page.html b/android/src/test/resources/csrf_page.html new file mode 100644 index 00000000..f27c3a17 --- /dev/null +++ b/android/src/test/resources/csrf_page.html @@ -0,0 +1,5 @@ +Lobsters
  1. 42
  2. 22
  3. 23
  4. 8
  5. 46
  6. 6
  7. 56
  8. 3
  9. 26
  10. 32
  11. 18
  12. 26
  13. 3
  14. 2
  15. 14
  16. 4
  17. 1
  18. 13
  19. 4
  20. 1
  21. 1
  22. 1
  23. 21
  24. 1
  25. 1
\ No newline at end of file diff --git a/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/ApiModule.kt b/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/ApiModule.kt index c513b953..20f829e1 100644 --- a/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/ApiModule.kt +++ b/api/src/main/kotlin/dev/msfjarvis/claw/api/injection/ApiModule.kt @@ -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 diff --git a/build-logic/src/main/kotlin/dev/msfjarvis/claw/gradle/versioning/VersioningPlugin.kt b/build-logic/src/main/kotlin/dev/msfjarvis/claw/gradle/versioning/VersioningPlugin.kt index f4f392b5..4894aedd 100644 --- a/build-logic/src/main/kotlin/dev/msfjarvis/claw/gradle/versioning/VersioningPlugin.kt +++ b/build-logic/src/main/kotlin/dev/msfjarvis/claw/gradle/versioning/VersioningPlugin.kt @@ -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. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1a41929..ff5215d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }