diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index 3870852..7f6f59e 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService import io.github.aakira.napier.Napier import io.ktor.client.* import io.ktor.client.plugins.* +import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.cookies.* @@ -15,6 +16,9 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* +import io.ktor.util.* +import io.ktor.utils.io.charsets.* +import io.ktor.utils.io.core.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -63,6 +67,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { expectSuccess = false } + return client } @@ -74,6 +79,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { client = createHttpClient() } + fun constructBasicAuthValue(credentials: BasicAuthCredentials): String { + val authString = "${credentials.username}:${credentials.password}" + val authBuf = authString.toByteArray(Charsets.UTF_8).encodeBase64() + + return "Basic $authBuf" + } + // Api version was introduces after the POST login, so when there is a version, it should be available private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1 private fun hasLoginInfo() = @@ -96,11 +108,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) private fun shouldHaveNewLogout() = @@ -114,9 +138,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { } private suspend fun maybeLogoutIfAvailable() = - responseOrSuccessIf404(client.tryToGet(url("/logout"))) + responseOrSuccessIf404(client.tryToGet(url("/logout")) { + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } + }) - private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current"))) + private suspend fun doLogout() = maybeResponse(client.tryToDelete(url("/api/session/current")) { + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } + }) suspend fun getItems( type: String, @@ -139,6 +177,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("updatedsince", updatedSince) parameter("items", items ?: appSettingsService.getItemsNumber()) parameter("offset", offset) + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun getItemsWithoutCatch(): StatusAndData> = @@ -149,6 +193,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { } parameter("type", "all") parameter("items", 1) + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun stats(): StatusAndData = @@ -157,6 +207,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun tags(): StatusAndData> = @@ -165,6 +221,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun update(): StatusAndData = @@ -173,6 +235,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun spouts(): StatusAndData> = @@ -181,6 +249,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun sourcesStats(): StatusAndData> = @@ -189,6 +263,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun sourcesDetailed(): StatusAndData> = @@ -197,10 +277,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun apiInformation(): StatusAndData = - bodyOrFailure(client.tryToGet(url("/api/about"))) + bodyOrFailure(client.tryToGet(url("/api/about")) { + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } + }) suspend fun markAsRead(id: String): SuccessResponse = maybeResponse(client.tryToPost(url("/mark/$id")) { @@ -208,6 +301,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun unmarkAsRead(id: String): SuccessResponse = @@ -216,6 +315,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun starr(id: String): SuccessResponse = @@ -224,6 +329,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun unstarr(id: String): SuccessResponse = @@ -232,6 +343,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } }) suspend fun markAllAsRead(ids: List): SuccessResponse = @@ -243,6 +360,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { append("password", appSettingsService.getPassword()) } ids.map { append("ids[]", it) } + }, + block = { + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } } )) @@ -278,6 +403,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { append("url", url) append("spout", spout) append(tagsParamName, tags) + }, + block = { + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } } ) @@ -315,6 +448,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { append("url", url) append("spout", spout) append(tagsParamName, tags) + }, + block = { + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append(HttpHeaders.Authorization, constructBasicAuthValue(BasicAuthCredentials(username = appSettingsService.getBasicUserName(), password = appSettingsService.getBasicPassword())) + ) + } + } } ) @@ -324,5 +465,18 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) } + if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) { + headers { + append( + HttpHeaders.Authorization, + constructBasicAuthValue( + BasicAuthCredentials( + username = appSettingsService.getBasicUserName(), + password = appSettingsService.getBasicPassword() + ) + ) + ) + } + } }) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt index 9699f97..22da4fa 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt @@ -10,7 +10,9 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { private var _publicAccess: Boolean? = null private var _baseUrl: String = "" private var _userName: String = "" + private var _basicUserName: String = "" private var _password: String = "" + private var _basicPassword: String = "" // User settings related private var _itemsCaching: Boolean? = null @@ -96,6 +98,20 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { return _password } + fun getBasicUserName(): String { + if (_basicUserName.isEmpty()) { + refreshBasicUsername() + } + return _basicUserName + } + + fun getBasicPassword(): String { + if (_basicPassword.isEmpty()) { + refreshBasicPassword() + } + return _basicPassword + } + fun getItemsNumber(): Int { if (_itemsNumber == null) { refreshItemsNumber() @@ -149,6 +165,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { _password = settings.getString(PASSWORD, "") } + private fun refreshBasicUsername() { + _basicUserName = settings.getString(BASIC_LOGIN, "") + } + + private fun refreshBasicPassword() { + _basicPassword = settings.getString(BASIC_PASSWORD, "") + } + private fun refreshArticleViewerEnabled() { _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true) } @@ -354,6 +378,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { fun refreshApiSettings() { refreshPassword() refreshUsername() + refreshBasicUsername() + refreshBasicPassword() refreshBaseUrl() refreshApiVersion() refreshPublicAccess() @@ -387,7 +413,17 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { login: String, password: String ) { - settings.putString(BASE_URL, url) + val regex = """\/\/(\D+):(\D+)@""".toRegex() + val matchResult = regex.find(url) + if (matchResult != null) { + val (basicLogin, basicPassword) = matchResult.destructured + settings.putString(BASIC_LOGIN, basicLogin) + settings.putString(BASIC_PASSWORD, basicPassword) + val urlWithoutBasicAuth = url.replace(regex, "//") + settings.putString(BASE_URL, urlWithoutBasicAuth) + } else { + settings.putString(BASE_URL, url) + } settings.putString(LOGIN, login) settings.putString(PASSWORD, password) refreshApiSettings() @@ -397,6 +433,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { settings.remove(BASE_URL) settings.remove(LOGIN) settings.remove(PASSWORD) + settings.remove(BASIC_LOGIN) + settings.remove(BASIC_PASSWORD) refreshApiSettings() } @@ -440,6 +478,10 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { const val PASSWORD = "password" + const val BASIC_LOGIN = "basic_login" + + const val BASIC_PASSWORD = "basic_password" + const val PREFER_ARTICLE_VIEWER = "prefer_article_viewer" const val CARD_VIEW_ACTIVE = "card_view_active" @@ -470,7 +512,6 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) { const val PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes" - const val INFINITE_LOADING = "infinite_loading" const val ITEMS_CACHING = "items_caching"