feat: Basic auth from url. Fixes #142 #143
| @@ -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<List<SelfossModel.Item>> = | ||||
| @@ -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<SelfossModel.Stats> = | ||||
| @@ -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<List<SelfossModel.Tag>> = | ||||
| @@ -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<String> = | ||||
| @@ -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<Map<String, SelfossModel.Spout>> = | ||||
| @@ -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<ArrayList<SelfossModel.SourceStats>> = | ||||
| @@ -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<ArrayList<SelfossModel.SourceDetail>> = | ||||
| @@ -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<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 = | ||||
|         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<String>): 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() | ||||
|                             ) | ||||
|                         ) | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user