CheckServerPresenter.kt
package chat.rocket.android.server.presentation
import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.helper.OauthHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.casLoginUrl
import chat.rocket.android.server.domain.gitlabUrl
import chat.rocket.android.server.domain.isCasAuthenticationEnabled
import chat.rocket.android.server.domain.isFacebookAuthenticationEnabled
import chat.rocket.android.server.domain.isGithubAuthenticationEnabled
import chat.rocket.android.server.domain.isGitlabAuthenticationEnabled
import chat.rocket.android.server.domain.isGoogleAuthenticationEnabled
import chat.rocket.android.server.domain.isLinkedinAuthenticationEnabled
import chat.rocket.android.server.domain.isLoginFormEnabled
import chat.rocket.android.server.domain.isRegistrationEnabledForNewUsers
import chat.rocket.android.server.domain.isWordpressAuthenticationEnabled
import chat.rocket.android.server.domain.wordpressUrl
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.RemoveAccountInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.infrastructure.ConnectionManager
import chat.rocket.android.server.infrastructure.ConnectionManagerFactory
import chat.rocket.android.server.infrastructure.RocketChatClientFactory
import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.casUrl
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.parseColor
import chat.rocket.android.util.extensions.samlUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatInvalidProtocolException
import chat.rocket.common.model.ServerInfo
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.serverInfo
import chat.rocket.core.internal.rest.settingsOauth
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Named
private const val SERVICE_NAME_FACEBOOK = "facebook"
private const val SERVICE_NAME_GITHUB = "github"
private const val SERVICE_NAME_GOOGLE = "google"
private const val SERVICE_NAME_LINKEDIN = "linkedin"
private const val SERVICE_NAME_GILAB = "gitlab"
private const val SERVICE_NAME_WORDPRESS = "wordpress"
abstract class CheckServerPresenter constructor(
private val strategy: CancelStrategy,
private val factory: RocketChatClientFactory,
@Named("currentServer") private val currentSavedServer: String?,
private val settingsInteractor: GetSettingsInteractor? = null,
private val serverInteractor: GetCurrentServerInteractor? = null,
private val localRepository: LocalRepository? = null,
private val removeAccountInteractor: RemoveAccountInteractor? = null,
private val tokenRepository: TokenRepository? = null,
private val managerFactory: ConnectionManagerFactory? = null,
private val dbManagerFactory: DatabaseManagerFactory? = null,
private val versionCheckView: VersionCheckView? = null,
private val tokenView: TokenView? = null,
private val navigator: MainNavigator? = null,
private val refreshSettingsInteractor: RefreshSettingsInteractor? = null
) {
private lateinit var currentServer: String
private var client: RocketChatClient? = null
private lateinit var settings: PublicSettings
private var connectionManager: ConnectionManager? = null
private var dbManager: DatabaseManager? = null
internal var state: String = ""
internal var facebookOauthUrl: String? = null
internal var githubOauthUrl: String? = null
internal var googleOauthUrl: String? = null
internal var linkedinOauthUrl: String? = null
internal var gitlabOauthUrl: String? = null
internal var wordpressOauthUrl: String? = null
internal var casLoginUrl: String? = null
internal var casToken: String? = null
internal var casServiceName: String? = null
internal var casServiceNameTextColor: Int = 0
internal var casServiceButtonColor: Int = 0
internal var customOauthUrl: String? = null
internal var customOauthServiceName: String? = null
internal var customOauthServiceNameTextColor: Int = 0
internal var customOauthServiceButtonColor: Int = 0
internal var samlUrl: String? = null
internal var samlToken: String? = null
internal var samlServiceName: String? = null
internal var samlServiceNameTextColor: Int = 0
internal var samlServiceButtonColor: Int = 0
internal var totalSocialAccountsEnabled = 0
internal var isLoginFormEnabled = false
internal var isNewAccountCreationEnabled = false
internal fun setupConnectionInfo(serverUrl: String) {
currentServer = serverUrl
client = factory.get(serverUrl)
managerFactory?.create(serverUrl)?.let {
connectionManager = it
}
dbManagerFactory?.create(serverUrl)?.let {
dbManager = it
}
}
internal suspend fun refreshServerAccounts() {
refreshSettingsInteractor?.refresh(currentServer)
settingsInteractor?.get(currentServer)?.let {
settings = it
}
state = ""
facebookOauthUrl = null
githubOauthUrl = null
googleOauthUrl = null
linkedinOauthUrl = null
gitlabOauthUrl = null
wordpressOauthUrl = null
casLoginUrl = null
casToken = null
casServiceName = null
casServiceNameTextColor = 0
casServiceButtonColor = 0
customOauthUrl = null
customOauthServiceName = null
customOauthServiceNameTextColor = 0
customOauthServiceButtonColor= 0
samlUrl = null
samlToken = null
samlServiceName = null
samlServiceNameTextColor = 0
samlServiceButtonColor = 0
totalSocialAccountsEnabled = 0
isLoginFormEnabled = false
isNewAccountCreationEnabled = false
}
internal fun checkServerInfo(serverUrl: String): Job {
return launchUI(strategy) {
try {
currentServer = serverUrl
retryIO(description = "serverInfo", times = 5) {
client?.serverInfo()?.let { serverInfo ->
if (serverInfo.redirected) {
versionCheckView?.updateServerUrl(serverInfo.url)
}
when (val version = checkServerVersion(serverInfo)) {
is Version.VersionOk -> {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: ${version.version})")
versionCheckView?.versionOk()
}
is Version.RecommendedVersionWarning -> {
Timber.i("Your server ${version.version} is bellow recommended version ${BuildConfig.RECOMMENDED_SERVER_VERSION}")
versionCheckView?.alertNotRecommendedVersion()
}
is Version.OutOfDateError -> {
Timber.i("Oops. Looks like your server ${version.version} is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
versionCheckView?.blockAndAlertNotRequiredVersion()
}
}
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error getting server info")
when (ex) {
is RocketChatInvalidProtocolException -> versionCheckView?.errorInvalidProtocol()
else -> versionCheckView?.errorCheckingServerVersion()
}
}
}
}
internal suspend fun checkEnabledAccounts(serverUrl: String) {
try {
retryIO("settingsOauth()") {
client?.settingsOauth()?.services?.let { services ->
if (services.isNotEmpty()) {
state = OauthHelper.getState()
checkEnabledOauthAccounts(services, serverUrl)
checkEnabledCasAccounts(services, serverUrl)
checkEnabledCustomOauthAccounts(services, serverUrl)
checkEnabledSamlAccounts(services, serverUrl)
}
}
}
} catch (exception: RocketChatException) {
Timber.e(exception)
}
}
/**
* Logout the user from the current server.
*/
internal fun logout() {
launchUI(strategy) {
try {
clearTokens()
retryIO("logout") { client?.logout() }
} catch (exception: RocketChatException) {
Timber.e(exception, "Error calling logout")
}
try {
connectionManager?.disconnect()
currentSavedServer?.let {
removeAccountInteractor?.remove(it)
tokenRepository?.remove(it)
}
withContext(Dispatchers.IO) { dbManager?.logout() }
navigator?.switchOrAddNewServer()
} catch (ex: Exception) {
Timber.e(ex, "Error cleaning up the session...")
}
}
}
private suspend fun clearTokens() {
serverInteractor?.clear()
val pushToken = localRepository?.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
try {
retryIO("unregisterPushToken") { client?.unregisterPushToken(pushToken) }
tokenView?.invalidateToken(pushToken)
} catch (ex: Exception) {
Timber.e(ex, "Error unregistering push token")
}
}
localRepository?.clearAllFromServer(currentServer)
}
private fun checkEnabledOauthAccounts(services: List<Map<String,Any>>, serverUrl: String) {
if (settings.isFacebookAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_FACEBOOK)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId ->
facebookOauthUrl =
OauthHelper.getFacebookOauthUrl(clientId, serverUrl, state)
totalSocialAccountsEnabled++
}
}
}
if (settings.isGithubAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_GITHUB)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId ->
githubOauthUrl =
OauthHelper.getGithubOauthUrl(clientId, state)
totalSocialAccountsEnabled++
}
}
}
if (settings.isGoogleAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_GOOGLE)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId ->
googleOauthUrl =
OauthHelper.getGoogleOauthUrl(clientId, serverUrl, state)
totalSocialAccountsEnabled++
}
}
}
if (settings.isLinkedinAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_LINKEDIN)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId ->
linkedinOauthUrl =
OauthHelper.getLinkedinOauthUrl(clientId, serverUrl, state)
totalSocialAccountsEnabled++
}
}
}
if (settings.isGitlabAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_GILAB)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId ->
gitlabOauthUrl = if (settings.gitlabUrl() != null) {
OauthHelper.getGitlabOauthUrl(
host = settings.gitlabUrl(),
clientId = clientId,
serverUrl = serverUrl,
state = state
)
} else {
OauthHelper.getGitlabOauthUrl(
clientId = clientId,
serverUrl = serverUrl,
state = state
)
}
totalSocialAccountsEnabled++
}
}
}
if (settings.isWordpressAuthenticationEnabled()) {
getServiceMap(services, SERVICE_NAME_WORDPRESS)?.let { serviceMap ->
getOauthClientId(serviceMap)?.let { clientId ->
wordpressOauthUrl =
if (settings.wordpressUrl().isNullOrEmpty()) {
OauthHelper.getWordpressComOauthUrl(
clientId,
serverUrl,
state
)
} else {
OauthHelper.getWordpressCustomOauthUrl(
getCustomOauthHost(serviceMap)
?: "https://public-api.wordpress.com",
getCustomOauthAuthorizePath(serviceMap)
?: "/oauth/authorize",
clientId,
serverUrl,
SERVICE_NAME_WORDPRESS,
state,
getCustomOauthScope(serviceMap) ?: "openid"
)
}
totalSocialAccountsEnabled++
}
}
}
}
private fun checkEnabledCasAccounts(services: List<Map<String,Any>>, serverUrl: String) {
if (settings.isCasAuthenticationEnabled()) {
casToken = generateRandomString(17)
casLoginUrl = settings.casLoginUrl().casUrl(serverUrl, casToken.toString())
getCasServices(services).let {
for (serviceMap in it) {
casServiceName = getServiceName(serviceMap)
val serviceNameTextColor = getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (casServiceName != null &&
serviceNameTextColor != null &&
serviceButtonColor != null
) {
casServiceNameTextColor = serviceNameTextColor
casServiceButtonColor = serviceButtonColor
totalSocialAccountsEnabled++
}
}
}
}
}
private fun checkEnabledCustomOauthAccounts(services: List<Map<String,Any>>, serverUrl: String) {
getCustomOauthServices(services).let {
for (serviceMap in it) {
customOauthServiceName = getCustomOauthServiceName(serviceMap)
val host = getCustomOauthHost(serviceMap)
val authorizePath = getCustomOauthAuthorizePath(serviceMap)
val clientId = getOauthClientId(serviceMap)
val scope = getCustomOauthScope(serviceMap)
val serviceNameTextColor =
getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (customOauthServiceName != null &&
host != null &&
authorizePath != null &&
clientId != null &&
scope != null &&
serviceNameTextColor != null &&
serviceButtonColor != null
) {
customOauthUrl = OauthHelper.getCustomOauthUrl(
host,
authorizePath,
clientId,
serverUrl,
customOauthServiceName.toString(),
state,
scope
)
customOauthServiceNameTextColor = serviceNameTextColor
customOauthServiceButtonColor = serviceButtonColor
totalSocialAccountsEnabled++
}
}
}
}
private fun checkEnabledSamlAccounts(services: List<Map<String,Any>>, serverUrl: String) {
getSamlServices(services).let {
samlToken = generateRandomString(17)
for (serviceMap in it) {
val provider = getSamlProvider(serviceMap)
samlServiceName = getServiceName(serviceMap)
val serviceNameTextColor =
getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (provider != null &&
samlServiceName != null &&
serviceNameTextColor != null &&
serviceButtonColor != null
) {
samlUrl = serverUrl.samlUrl(provider, samlToken.toString())
samlServiceNameTextColor = serviceNameTextColor
samlServiceButtonColor = serviceButtonColor
totalSocialAccountsEnabled++
}
}
}
}
internal fun checkIfLoginFormIsEnabled() {
if (settings.isLoginFormEnabled()) {
isLoginFormEnabled = true
}
}
internal fun checkIfCreateNewAccountIsEnabled() {
if (settings.isRegistrationEnabledForNewUsers() && settings.isLoginFormEnabled()) {
isNewAccountCreationEnabled = true
}
}
/** Returns an OAuth service map given a [serviceName].
*
* @param listMap The list of [Map] to get the service from.
* @param serviceName The service name to get in the [listMap]
* @return The OAuth service map or null otherwise.
*/
private fun getServiceMap(
listMap: List<Map<String, Any>>,
serviceName: String
): Map<String, Any>? = listMap.find { map -> map.containsValue(serviceName) }
/**
* Returns the OAuth client ID of a [serviceMap].
* REMARK: This function works for common OAuth providers (Google, Facebook, GitHub and so on)
* as well as custom OAuth.
*
* @param serviceMap The service map to get the OAuth client ID.
* @return The OAuth client ID or null otherwise.
*/
private fun getOauthClientId(serviceMap: Map<String, Any>): String? =
serviceMap["clientId"] as? String ?: serviceMap["appId"] as? String
/**
* Returns a custom OAuth service list.
*
* @return A custom OAuth service list, otherwise an empty list if there is no custom OAuth service.
*/
private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> =
listMap.filter { map -> map["custom"] == true }
/** Returns the custom OAuth service host.
*
* @param serviceMap The service map to get the custom OAuth service host.
* @return The custom OAuth service host, otherwise null.
*/
private fun getCustomOauthHost(serviceMap: Map<String, Any>): String? =
serviceMap["serverURL"] as? String
/** Returns the custom OAuth service authorize path.
*
* @param serviceMap The service map to get the custom OAuth service authorize path.
* @return The custom OAuth service authorize path, otherwise null.
*/
private fun getCustomOauthAuthorizePath(serviceMap: Map<String, Any>): String? =
serviceMap["authorizePath"] as? String
/** Returns the custom OAuth service scope.
*
* @param serviceMap The service map to get the custom OAuth service scope.
* @return The custom OAuth service scope, otherwise null.
*/
private fun getCustomOauthScope(serviceMap: Map<String, Any>): String? =
serviceMap["scope"] as? String
/** Returns the text of the custom OAuth service.
*
* @param serviceMap The service map to get the text of the custom OAuth service.
* @return The text of the custom OAuth service, otherwise null.
*/
private fun getCustomOauthServiceName(serviceMap: Map<String, Any>): String? =
serviceMap["service"] as? String
/**
* Returns a CAS service list.
*
* @return A CAS service list, otherwise an empty list if there is no CAS service.
*/
private fun getCasServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> =
listMap.filter { map -> map["service"] == "cas" }
/**
* Returns a SAML OAuth service list.
*
* @return A SAML service list, otherwise an empty list if there is no SAML OAuth service.
*/
private fun getSamlServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> =
listMap.filter { map -> map["service"] == "saml" }
/**
* Returns the SAML provider.
*
* @param serviceMap The service map to provider from.
* @return The SAML provider, otherwise null.
*/
private fun getSamlProvider(serviceMap: Map<String, Any>): String? =
(serviceMap["clientConfig"] as Map<*, *>)["provider"] as? String
/**
* Returns the text of the SAML service.
* REMARK: This can be used SAML or CAS.
*
* @param serviceMap The service map to get the text of the SAML service.
* @return The text of the SAML service, otherwise null.
*/
private fun getServiceName(serviceMap: Map<String, Any>): String? =
serviceMap["buttonLabelText"] as? String
/**
* Returns the text color of the service name.
* REMARK: This can be used for custom OAuth, SAML or CAS.
*
* @param serviceMap The service map to get the text color from.
* @return The text color of the service (custom OAuth or SAML), otherwise null.
*/
private fun getServiceNameColor(serviceMap: Map<String, Any>): Int? =
(serviceMap["buttonLabelColor"] as? String)?.parseColor()
/**
* Returns the button color of the service name.
* REMARK: This can be used for custom OAuth, SAML or CAS.
*
* @param serviceMap The service map to get the button color from.
* @return The button color of the service (custom OAuth or SAML), otherwise null.
*/
private fun getServiceButtonColor(serviceMap: Map<String, Any>): Int? =
(serviceMap["buttonColor"] as? String)?.parseColor()
private fun checkServerVersion(serverInfo: ServerInfo): Version {
val thisServerVersion = serverInfo.version
val isRequiredVersion = isRequiredServerVersion(thisServerVersion)
val isRecommendedVersion = isRecommendedServerVersion(thisServerVersion)
return if (isRequiredVersion) {
if (isRecommendedVersion) {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: $thisServerVersion)")
Version.VersionOk(thisServerVersion)
} else {
Version.RecommendedVersionWarning(thisServerVersion)
}
} else {
Version.OutOfDateError(thisServerVersion)
}
}
private fun isRequiredServerVersion(version: String): Boolean {
return isMinimumVersion(version, getVersionDistilled(BuildConfig.REQUIRED_SERVER_VERSION))
}
private fun isRecommendedServerVersion(version: String): Boolean {
return isMinimumVersion(
version,
getVersionDistilled(BuildConfig.RECOMMENDED_SERVER_VERSION)
)
}
private fun isMinimumVersion(version: String, required: VersionInfo): Boolean {
val thisVersion = getVersionDistilled(version)
with(thisVersion) {
if (major < required.major) {
return false
} else if (major > required.major) {
return true
}
if (minor < required.minor) {
return false
} else if (minor > required.minor) {
return true
}
return update >= required.update
}
}
private fun getVersionDistilled(version: String): VersionInfo {
var split = version.split("-")
if (split.isEmpty()) {
return VersionInfo(0, 0, 0, null, "0.0.0")
}
val ver = split[0]
var release: String? = null
if (split.size > 1) {
release = split[1]
}
split = ver.split(".")
val major = getVersionNumber(split, 0)
val minor = getVersionNumber(split, 1)
val update = getVersionNumber(split, 2)
return VersionInfo(
major = major,
minor = minor,
update = update,
release = release,
full = version
)
}
private fun getVersionNumber(split: List<String>, index: Int): Int {
return try {
split.getOrNull(index)?.toInt() ?: 0
} catch (ex: NumberFormatException) {
0
}
}
sealed class Version(val version: String) {
data class VersionOk(private val currentVersion: String) : Version(currentVersion)
data class RecommendedVersionWarning(private val currentVersion: String) :
Version(currentVersion)
data class OutOfDateError(private val currentVersion: String) : Version(currentVersion)
}
}