feat: Basic auth from url. Fixes #142 (#143)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

Co-authored-by: aminecmi <aminecmi@gmail.com>
Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/143
This commit is contained in:
Amine Louveau 2023-05-10 19:19:11 +00:00
parent 66ef1ccf32
commit 2a2d1047b4
2 changed files with 200 additions and 5 deletions

View File

@ -7,6 +7,7 @@ import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier import io.github.aakira.napier.Napier
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.* import io.ktor.client.plugins.cookies.*
@ -15,6 +16,9 @@ import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.* 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -63,6 +67,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
expectSuccess = false expectSuccess = false
} }
return client return client
} }
@ -74,6 +79,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient() 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 // 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 shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() = private fun hasLoginInfo() =
@ -96,11 +108,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) { private suspend fun getLogin() = maybeResponse(client.tryToGet(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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")) { private suspend fun postLogin() = maybeResponse(client.tryToPost(url("/login")) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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() = private fun shouldHaveNewLogout() =
@ -114,9 +138,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
private suspend fun maybeLogoutIfAvailable() = 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( suspend fun getItems(
type: String, type: String,
@ -139,6 +177,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("updatedsince", updatedSince) parameter("updatedsince", updatedSince)
parameter("items", items ?: appSettingsService.getItemsNumber()) parameter("items", items ?: appSettingsService.getItemsNumber())
parameter("offset", offset) 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<List<SelfossModel.Item>> = suspend fun getItemsWithoutCatch(): StatusAndData<List<SelfossModel.Item>> =
@ -149,6 +193,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
parameter("type", "all") parameter("type", "all")
parameter("items", 1) 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<SelfossModel.Stats> = suspend fun stats(): StatusAndData<SelfossModel.Stats> =
@ -157,6 +207,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<List<SelfossModel.Tag>> = suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
@ -165,6 +221,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<String> = suspend fun update(): StatusAndData<String> =
@ -173,6 +235,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<Map<String, SelfossModel.Spout>> = suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
@ -181,6 +249,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<ArrayList<SelfossModel.SourceStats>> = suspend fun sourcesStats(): StatusAndData<ArrayList<SelfossModel.SourceStats>> =
@ -189,6 +263,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<ArrayList<SelfossModel.SourceDetail>> = suspend fun sourcesDetailed(): StatusAndData<ArrayList<SelfossModel.SourceDetail>> =
@ -197,10 +277,23 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<SelfossModel.ApiInformation> = suspend fun apiInformation(): StatusAndData<SelfossModel.ApiInformation> =
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 = suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.tryToPost(url("/mark/$id")) { maybeResponse(client.tryToPost(url("/mark/$id")) {
@ -208,6 +301,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun unmarkAsRead(id: String): SuccessResponse =
@ -216,6 +315,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun starr(id: String): SuccessResponse =
@ -224,6 +329,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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 = suspend fun unstarr(id: String): SuccessResponse =
@ -232,6 +343,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) 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<String>): SuccessResponse = suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
@ -243,6 +360,14 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
append("password", appSettingsService.getPassword()) append("password", appSettingsService.getPassword())
} }
ids.map { append("ids[]", it) } 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("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) 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("url", url)
append("spout", spout) append("spout", spout)
append(tagsParamName, tags) 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("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
} }
if (appSettingsService.getBasicUserName().isNotEmpty() && appSettingsService.getBasicPassword().isNotEmpty()) {
headers {
append(
HttpHeaders.Authorization,
constructBasicAuthValue(
BasicAuthCredentials(
username = appSettingsService.getBasicUserName(),
password = appSettingsService.getBasicPassword()
)
)
)
}
}
}) })
} }

View File

@ -10,7 +10,9 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
private var _publicAccess: Boolean? = null private var _publicAccess: Boolean? = null
private var _baseUrl: String = "" private var _baseUrl: String = ""
private var _userName: String = "" private var _userName: String = ""
private var _basicUserName: String = ""
private var _password: String = "" private var _password: String = ""
private var _basicPassword: String = ""
// User settings related // User settings related
private var _itemsCaching: Boolean? = null private var _itemsCaching: Boolean? = null
@ -96,6 +98,20 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
return _password return _password
} }
fun getBasicUserName(): String {
if (_basicUserName.isEmpty()) {
refreshBasicUsername()
}
return _basicUserName
}
fun getBasicPassword(): String {
if (_basicPassword.isEmpty()) {
refreshBasicPassword()
}
return _basicPassword
}
fun getItemsNumber(): Int { fun getItemsNumber(): Int {
if (_itemsNumber == null) { if (_itemsNumber == null) {
refreshItemsNumber() refreshItemsNumber()
@ -149,6 +165,14 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
_password = settings.getString(PASSWORD, "") _password = settings.getString(PASSWORD, "")
} }
private fun refreshBasicUsername() {
_basicUserName = settings.getString(BASIC_LOGIN, "")
}
private fun refreshBasicPassword() {
_basicPassword = settings.getString(BASIC_PASSWORD, "")
}
private fun refreshArticleViewerEnabled() { private fun refreshArticleViewerEnabled() {
_articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true) _articleViewer = settings.getBoolean(PREFER_ARTICLE_VIEWER, true)
} }
@ -354,6 +378,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
fun refreshApiSettings() { fun refreshApiSettings() {
refreshPassword() refreshPassword()
refreshUsername() refreshUsername()
refreshBasicUsername()
refreshBasicPassword()
refreshBaseUrl() refreshBaseUrl()
refreshApiVersion() refreshApiVersion()
refreshPublicAccess() refreshPublicAccess()
@ -387,7 +413,17 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
login: String, login: String,
password: String password: String
) { ) {
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(BASE_URL, url)
}
settings.putString(LOGIN, login) settings.putString(LOGIN, login)
settings.putString(PASSWORD, password) settings.putString(PASSWORD, password)
refreshApiSettings() refreshApiSettings()
@ -397,6 +433,8 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
settings.remove(BASE_URL) settings.remove(BASE_URL)
settings.remove(LOGIN) settings.remove(LOGIN)
settings.remove(PASSWORD) settings.remove(PASSWORD)
settings.remove(BASIC_LOGIN)
settings.remove(BASIC_PASSWORD)
refreshApiSettings() refreshApiSettings()
} }
@ -440,6 +478,10 @@ class AppSettingsService(acraSenderServiceProcess: Boolean = false) {
const val PASSWORD = "password" 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 PREFER_ARTICLE_VIEWER = "prefer_article_viewer"
const val CARD_VIEW_ACTIVE = "card_view_active" 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 PERIODIC_REFRESH_MINUTES = "periodic_refresh_minutes"
const val INFINITE_LOADING = "infinite_loading" const val INFINITE_LOADING = "infinite_loading"
const val ITEMS_CACHING = "items_caching" const val ITEMS_CACHING = "items_caching"