mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
BIT-621: Add URI matching for autofill (#842)
This commit is contained in:
parent
0e5e6b4444
commit
0c6ea8d18d
19 changed files with 26674 additions and 95 deletions
|
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
|
||||||
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
@ -60,10 +61,12 @@ object AutofillModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun providesAutofillCipherProvider(
|
fun providesAutofillCipherProvider(
|
||||||
authRepository: AuthRepository,
|
authRepository: AuthRepository,
|
||||||
|
cipherMatchingManager: CipherMatchingManager,
|
||||||
vaultRepository: VaultRepository,
|
vaultRepository: VaultRepository,
|
||||||
): AutofillCipherProvider =
|
): AutofillCipherProvider =
|
||||||
AutofillCipherProviderImpl(
|
AutofillCipherProviderImpl(
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
|
cipherMatchingManager = cipherMatchingManager,
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import com.bitwarden.core.CipherType
|
||||||
import com.bitwarden.core.CipherView
|
import com.bitwarden.core.CipherView
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
|
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.first
|
||||||
*/
|
*/
|
||||||
class AutofillCipherProviderImpl(
|
class AutofillCipherProviderImpl(
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
|
private val cipherMatchingManager: CipherMatchingManager,
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
) : AutofillCipherProvider {
|
) : AutofillCipherProvider {
|
||||||
private val activeUserId: String? get() = authRepository.activeUserId
|
private val activeUserId: String? get() = authRepository.activeUserId
|
||||||
|
@ -35,7 +36,8 @@ class AutofillCipherProviderImpl(
|
||||||
return cipherViews
|
return cipherViews
|
||||||
.mapNotNull { cipherView ->
|
.mapNotNull { cipherView ->
|
||||||
cipherView
|
cipherView
|
||||||
.takeIf { cipherView.type == CipherType.CARD }
|
// We only care about non-deleted card ciphers.
|
||||||
|
.takeIf { cipherView.type == CipherType.CARD && cipherView.deletedDate == null }
|
||||||
?.let { nonNullCipherView ->
|
?.let { nonNullCipherView ->
|
||||||
AutofillCipher.Card(
|
AutofillCipher.Card(
|
||||||
name = nonNullCipherView.name,
|
name = nonNullCipherView.name,
|
||||||
|
@ -54,24 +56,23 @@ class AutofillCipherProviderImpl(
|
||||||
uri: String,
|
uri: String,
|
||||||
): List<AutofillCipher.Login> {
|
): List<AutofillCipher.Login> {
|
||||||
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
|
||||||
|
// We only care about non-deleted login ciphers.
|
||||||
|
val loginCiphers = cipherViews
|
||||||
|
.filter { it.type == CipherType.LOGIN && it.deletedDate == null }
|
||||||
|
|
||||||
return cipherViews
|
return cipherMatchingManager
|
||||||
.mapNotNull { cipherView ->
|
// Filter for ciphers that match the uri in some way.
|
||||||
cipherView
|
.filterCiphersForMatches(
|
||||||
.takeIf { cipherView.type == CipherType.LOGIN }
|
ciphers = loginCiphers,
|
||||||
// TODO: Get global URI matching value from settings repo and
|
matchUri = uri,
|
||||||
// TODO: perform more complex URI matching here (BIT-1461).
|
)
|
||||||
?.takeIfUriMatches(
|
.map { cipherView ->
|
||||||
uri = uri,
|
AutofillCipher.Login(
|
||||||
)
|
name = cipherView.name,
|
||||||
?.let { nonNullCipherView ->
|
password = cipherView.login?.password.orEmpty(),
|
||||||
AutofillCipher.Login(
|
subtitle = cipherView.subtitle.orEmpty(),
|
||||||
name = nonNullCipherView.name,
|
username = cipherView.login?.username.orEmpty(),
|
||||||
password = nonNullCipherView.login?.password.orEmpty(),
|
)
|
||||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
|
||||||
username = nonNullCipherView.login?.username.orEmpty(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
|
||||||
|
|
||||||
|
import com.bitwarden.core.CipherView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager for matching ciphers based on special criteria.
|
||||||
|
*/
|
||||||
|
interface CipherMatchingManager {
|
||||||
|
/**
|
||||||
|
* Filter [ciphers] for entries that match the [matchUri] in some fashion.
|
||||||
|
*/
|
||||||
|
suspend fun filterCiphersForMatches(
|
||||||
|
ciphers: List<CipherView>,
|
||||||
|
matchUri: String,
|
||||||
|
): List<CipherView>
|
||||||
|
}
|
|
@ -0,0 +1,274 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bitwarden.core.CipherView
|
||||||
|
import com.bitwarden.core.LoginUriView
|
||||||
|
import com.bitwarden.core.UriMatchType
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getHostWithPortOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||||
|
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.toSdkUriMatchType
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlin.text.Regex
|
||||||
|
import kotlin.text.RegexOption
|
||||||
|
import kotlin.text.isNullOrBlank
|
||||||
|
import kotlin.text.lowercase
|
||||||
|
import kotlin.text.matches
|
||||||
|
import kotlin.text.startsWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default [CipherMatchingManager] implementation. This class is responsible for matching
|
||||||
|
* ciphers based on special criteria.
|
||||||
|
*/
|
||||||
|
class CipherMatchingManagerImpl(
|
||||||
|
private val context: Context,
|
||||||
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val vaultRepository: VaultRepository,
|
||||||
|
) : CipherMatchingManager {
|
||||||
|
override suspend fun filterCiphersForMatches(
|
||||||
|
ciphers: List<CipherView>,
|
||||||
|
matchUri: String,
|
||||||
|
): List<CipherView> {
|
||||||
|
val equivalentDomainsData = vaultRepository
|
||||||
|
.domainsStateFlow
|
||||||
|
.mapNotNull { it.data }
|
||||||
|
.first()
|
||||||
|
|
||||||
|
val isAndroidApp = matchUri.isAndroidApp()
|
||||||
|
val defaultUriMatchType = settingsRepository.defaultUriMatchType.toSdkUriMatchType()
|
||||||
|
val domain = matchUri
|
||||||
|
.getDomainOrNull(context = context)
|
||||||
|
?.lowercase()
|
||||||
|
|
||||||
|
// Retrieve domains that are considered equivalent to the specified matchUri for cipher
|
||||||
|
// comparison. If a cipher doesn't have a URI matching the matchUri, but matches a domain in
|
||||||
|
// matchingDomains, it's considered a match.
|
||||||
|
val matchingDomains = getMatchingDomains(
|
||||||
|
domainsData = equivalentDomainsData,
|
||||||
|
isAndroidApp = isAndroidApp,
|
||||||
|
matchDomain = domain,
|
||||||
|
matchUri = matchUri,
|
||||||
|
)
|
||||||
|
|
||||||
|
val exactMatchingCiphers = mutableListOf<CipherView>()
|
||||||
|
val fuzzyMatchingCiphers = mutableListOf<CipherView>()
|
||||||
|
|
||||||
|
ciphers
|
||||||
|
.forEach { cipherView ->
|
||||||
|
val matchResult = checkForCipherMatch(
|
||||||
|
cipherView = cipherView,
|
||||||
|
context = context,
|
||||||
|
defaultUriMatchType = defaultUriMatchType,
|
||||||
|
isAndroidApp = isAndroidApp,
|
||||||
|
matchUri = matchUri,
|
||||||
|
matchingDomains = matchingDomains,
|
||||||
|
)
|
||||||
|
|
||||||
|
when (matchResult) {
|
||||||
|
MatchResult.EXACT -> exactMatchingCiphers.add(cipherView)
|
||||||
|
MatchResult.FUZZY -> fuzzyMatchingCiphers.add(cipherView)
|
||||||
|
MatchResult.NONE -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exactMatchingCiphers + fuzzyMatchingCiphers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of domains that match the specified domain. If the domain is contained within
|
||||||
|
* the [DomainsData], this will return all matching domains. Otherwise, it will return
|
||||||
|
* [matchDomain] or [matchUri] depending on [isAndroidApp].
|
||||||
|
*/
|
||||||
|
private fun getMatchingDomains(
|
||||||
|
domainsData: DomainsData,
|
||||||
|
isAndroidApp: Boolean,
|
||||||
|
matchDomain: String?,
|
||||||
|
matchUri: String,
|
||||||
|
): MatchingDomains {
|
||||||
|
val androidAppWebHost = matchUri.getWebHostFromAndroidUriOrNull()
|
||||||
|
val equivalentDomainsList = domainsData
|
||||||
|
.equivalentDomains
|
||||||
|
.plus(
|
||||||
|
elements = domainsData
|
||||||
|
.globalEquivalentDomains
|
||||||
|
.map { it.domains },
|
||||||
|
)
|
||||||
|
|
||||||
|
val exactMatchDomains = mutableListOf<String>()
|
||||||
|
val fuzzyMatchDomains = mutableListOf<String>()
|
||||||
|
equivalentDomainsList
|
||||||
|
.forEach { equivalentDomains ->
|
||||||
|
when {
|
||||||
|
isAndroidApp && equivalentDomains.contains(matchUri) -> {
|
||||||
|
exactMatchDomains.addAll(equivalentDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
isAndroidApp && equivalentDomains.contains(androidAppWebHost) -> {
|
||||||
|
fuzzyMatchDomains.addAll(equivalentDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
!isAndroidApp && equivalentDomains.contains(matchDomain) -> {
|
||||||
|
exactMatchDomains.addAll(equivalentDomains)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no equivalent domains, add a version of the original URI to the list.
|
||||||
|
when {
|
||||||
|
exactMatchDomains.isEmpty() && isAndroidApp -> exactMatchDomains.add(matchUri)
|
||||||
|
exactMatchDomains.isEmpty() && matchDomain != null -> exactMatchDomains.add(matchDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatchingDomains(
|
||||||
|
exactMatches = exactMatchDomains,
|
||||||
|
fuzzyMatches = fuzzyMatchDomains,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if [cipherView] matches [matchUri] in some way. The returned [MatchResult] will
|
||||||
|
* provide details on the match quality.
|
||||||
|
*
|
||||||
|
* @param cipherView The cipher to be judged for a match.
|
||||||
|
* @param context A context for getting string resources.
|
||||||
|
* @param defaultUriMatchType The global default [UriMatchType].
|
||||||
|
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||||
|
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||||
|
* @param matchUri The uri that this cipher is being matched to.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun checkForCipherMatch(
|
||||||
|
cipherView: CipherView,
|
||||||
|
context: Context,
|
||||||
|
defaultUriMatchType: UriMatchType,
|
||||||
|
isAndroidApp: Boolean,
|
||||||
|
matchingDomains: MatchingDomains,
|
||||||
|
matchUri: String,
|
||||||
|
): MatchResult {
|
||||||
|
val matchResults = cipherView
|
||||||
|
.login
|
||||||
|
?.uris
|
||||||
|
?.map { loginUriView ->
|
||||||
|
loginUriView.checkForMatch(
|
||||||
|
context = context,
|
||||||
|
defaultUriMatchType = defaultUriMatchType,
|
||||||
|
isAndroidApp = isAndroidApp,
|
||||||
|
matchingDomains = matchingDomains,
|
||||||
|
matchUri = matchUri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchResults
|
||||||
|
?.firstOrNull { it == MatchResult.EXACT }
|
||||||
|
?: matchResults
|
||||||
|
?.firstOrNull { it == MatchResult.FUZZY }
|
||||||
|
?: MatchResult.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see how well this [LoginUriView] matches [matchUri].
|
||||||
|
*
|
||||||
|
* @param context A context for getting app information.
|
||||||
|
* @param defaultUriMatchType The global default [UriMatchType].
|
||||||
|
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||||
|
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||||
|
* @param matchUri The uri that this [LoginUriView] is being matched to.
|
||||||
|
*/
|
||||||
|
private fun LoginUriView.checkForMatch(
|
||||||
|
context: Context,
|
||||||
|
defaultUriMatchType: UriMatchType,
|
||||||
|
isAndroidApp: Boolean,
|
||||||
|
matchingDomains: MatchingDomains,
|
||||||
|
matchUri: String,
|
||||||
|
): MatchResult {
|
||||||
|
val matchType = this.match ?: defaultUriMatchType
|
||||||
|
val loginViewUri = this.uri
|
||||||
|
|
||||||
|
return if (!loginViewUri.isNullOrBlank()) {
|
||||||
|
when (matchType) {
|
||||||
|
UriMatchType.DOMAIN -> {
|
||||||
|
checkUriForDomainMatch(
|
||||||
|
context = context,
|
||||||
|
isAndroidApp = isAndroidApp,
|
||||||
|
matchingDomains = matchingDomains,
|
||||||
|
uri = loginViewUri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UriMatchType.EXACT -> exactIfTrue(loginViewUri == matchUri)
|
||||||
|
|
||||||
|
UriMatchType.HOST -> {
|
||||||
|
val loginUriHost = loginViewUri.getHostWithPortOrNull()
|
||||||
|
val matchUriHost = matchUri.getHostWithPortOrNull()
|
||||||
|
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
UriMatchType.NEVER -> MatchResult.NONE
|
||||||
|
|
||||||
|
UriMatchType.REGULAR_EXPRESSION -> {
|
||||||
|
val pattern = Regex(loginViewUri, RegexOption.IGNORE_CASE)
|
||||||
|
exactIfTrue(matchUri.matches(pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
UriMatchType.STARTS_WITH -> exactIfTrue(matchUri.startsWith(loginViewUri))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MatchResult.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if [uri] matches [matchingDomains] in some way.
|
||||||
|
*/
|
||||||
|
private fun checkUriForDomainMatch(
|
||||||
|
isAndroidApp: Boolean,
|
||||||
|
context: Context,
|
||||||
|
matchingDomains: MatchingDomains,
|
||||||
|
uri: String,
|
||||||
|
): MatchResult = when {
|
||||||
|
matchingDomains.exactMatches.contains(uri) -> MatchResult.EXACT
|
||||||
|
isAndroidApp && matchingDomains.fuzzyMatches.contains(uri) -> MatchResult.FUZZY
|
||||||
|
else -> {
|
||||||
|
val domain = uri
|
||||||
|
.getDomainOrNull(context = context)
|
||||||
|
?.lowercase()
|
||||||
|
|
||||||
|
// We only care about fuzzy matches if we are isAndroidApp is true because the fuzzu
|
||||||
|
// matches are generated using a app URI derived host.
|
||||||
|
when {
|
||||||
|
matchingDomains.exactMatches.contains(domain) -> MatchResult.EXACT
|
||||||
|
isAndroidApp && matchingDomains.fuzzyMatches.contains(domain) -> MatchResult.FUZZY
|
||||||
|
else -> MatchResult.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple function to return [MatchResult.EXACT] if [condition] is true, and
|
||||||
|
* [MatchResult.NONE] otherwise.
|
||||||
|
*/
|
||||||
|
private fun exactIfTrue(condition: Boolean): MatchResult =
|
||||||
|
if (condition) MatchResult.EXACT else MatchResult.NONE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience data class for holding domain matches.
|
||||||
|
*/
|
||||||
|
private data class MatchingDomains(
|
||||||
|
val exactMatches: List<String>,
|
||||||
|
val fuzzyMatches: List<String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A enum to represent the quality of a match.
|
||||||
|
*/
|
||||||
|
private enum class MatchResult {
|
||||||
|
EXACT,
|
||||||
|
FUZZY,
|
||||||
|
NONE,
|
||||||
|
}
|
|
@ -22,11 +22,15 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -48,6 +52,19 @@ object PlatformManagerModule {
|
||||||
fun provideAppForegroundManager(): AppForegroundManager =
|
fun provideAppForegroundManager(): AppForegroundManager =
|
||||||
AppForegroundManagerImpl()
|
AppForegroundManagerImpl()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesCipherMatchingManager(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
settingsRepository: SettingsRepository,
|
||||||
|
vaultRepository: VaultRepository,
|
||||||
|
): CipherMatchingManager =
|
||||||
|
CipherMatchingManagerImpl(
|
||||||
|
context = context,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
vaultRepository = vaultRepository,
|
||||||
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideClock(): Clock = Clock.systemDefaultZone()
|
fun provideClock(): Clock = Clock.systemDefaultZone()
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class containing the result of parsing the URL.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* - URL: m.google.com
|
||||||
|
* - domain: google.com
|
||||||
|
* - secondLevelDomain: google
|
||||||
|
* - subDomain: m
|
||||||
|
* - topLevelDomain: com
|
||||||
|
*
|
||||||
|
* @property secondLevelDomain The second-level domain of the URL, if it exists.
|
||||||
|
* @property subDomain The subdomain of the URL.
|
||||||
|
* @property topLevelDomain The top-level domain (TLD) of the URL.
|
||||||
|
*/
|
||||||
|
data class DomainName(
|
||||||
|
val secondLevelDomain: String?,
|
||||||
|
val subDomain: String?,
|
||||||
|
val topLevelDomain: String,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The domain of the URL, constructed from the second-level and top-level domains.
|
||||||
|
*/
|
||||||
|
val domain: String
|
||||||
|
get() = "$secondLevelDomain.$topLevelDomain"
|
||||||
|
}
|
|
@ -68,18 +68,3 @@ private val CardView.subtitleCardNumber: String?
|
||||||
*/
|
*/
|
||||||
private val String?.isAmEx: Boolean
|
private val String?.isAmEx: Boolean
|
||||||
get() = this?.startsWith("34") == true || this?.startsWith("37") == true
|
get() = this?.startsWith("34") == true || this?.startsWith("37") == true
|
||||||
|
|
||||||
/**
|
|
||||||
* Take this [CipherView] if its uri matches [uri]. Otherwise, return null.
|
|
||||||
*/
|
|
||||||
fun CipherView.takeIfUriMatches(
|
|
||||||
uri: String,
|
|
||||||
): CipherView? =
|
|
||||||
// TODO: Pass global URI matching value from settings (BIT-1461)
|
|
||||||
this
|
|
||||||
.takeIf {
|
|
||||||
// TODO: perform comprehensive URI matching (BIT-1461)
|
|
||||||
login
|
|
||||||
?.uris
|
|
||||||
?.any { it.uri == uri } == true
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The protocol for and Android app URI.
|
||||||
|
*/
|
||||||
|
private const val ANDROID_APP_PROTOCOL: String = "androidapp://"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try creating a [URI] out of this [String]. If it fails, return null.
|
||||||
|
*/
|
||||||
|
fun String.toUriOrNull(): URI? =
|
||||||
|
try {
|
||||||
|
URI(this)
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this [String] represents an android app URI.
|
||||||
|
*/
|
||||||
|
fun String.isAndroidApp(): Boolean =
|
||||||
|
this.startsWith(ANDROID_APP_PROTOCOL)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try and extract the web host from this [String] if it represents an Android app.
|
||||||
|
*/
|
||||||
|
fun String.getWebHostFromAndroidUriOrNull(): String? =
|
||||||
|
if (this.isAndroidApp()) {
|
||||||
|
val components = this
|
||||||
|
.replace(ANDROID_APP_PROTOCOL, "")
|
||||||
|
.split('.')
|
||||||
|
|
||||||
|
if (components.size > 1) {
|
||||||
|
"${components[1]}.${components[0]}"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the domain name from this [String] if possible, otherwise return null.
|
||||||
|
*/
|
||||||
|
fun String.getDomainOrNull(context: Context): String? =
|
||||||
|
this
|
||||||
|
.toUriOrNull()
|
||||||
|
?.parseDomainOrNull(context = context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the host with port from this [String] if possible, otherwise return null.
|
||||||
|
*/
|
||||||
|
@OmitFromCoverage
|
||||||
|
fun String.getHostWithPortOrNull(): String? =
|
||||||
|
this
|
||||||
|
.toUriOrNull()
|
||||||
|
?.let { uri ->
|
||||||
|
val host = uri.host
|
||||||
|
val port = uri.port
|
||||||
|
|
||||||
|
if (host != null && port != -1) {
|
||||||
|
"$host:$port"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the indices of the last occurrences of [substring] within this [String]. Return null if no
|
||||||
|
* occurrences are found.
|
||||||
|
*/
|
||||||
|
fun String.findLastSubstringIndicesOrNull(substring: String): IntRange? {
|
||||||
|
val lastIndex = this.lastIndexOf(substring)
|
||||||
|
|
||||||
|
return if (lastIndex != -1) {
|
||||||
|
val endIndex = lastIndex + substring.length - 1
|
||||||
|
IntRange(lastIndex, endIndex)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.DomainName
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A regular expression that matches IP addresses.
|
||||||
|
*/
|
||||||
|
private const val IP_REGEX: String =
|
||||||
|
"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
|
||||||
|
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
|
||||||
|
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
|
||||||
|
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the base domain from the URL. Returns null if unavailable.
|
||||||
|
*/
|
||||||
|
fun URI.parseDomainOrNull(context: Context): String? {
|
||||||
|
val host = this?.host ?: return null
|
||||||
|
val isIpAddress = host.matches(IP_REGEX.toRegex())
|
||||||
|
|
||||||
|
return if (host == "localhost" || isIpAddress) {
|
||||||
|
host
|
||||||
|
} else {
|
||||||
|
parseDomainNameOrNullInternal(
|
||||||
|
context = context,
|
||||||
|
host = host,
|
||||||
|
)
|
||||||
|
?.domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a URL to get the breakdown of a URL's domain. Returns null if invalid.
|
||||||
|
*/
|
||||||
|
fun URI.parseDomainNameOrNull(context: Context): DomainName? =
|
||||||
|
this
|
||||||
|
// URI is a platform type and host can be null.
|
||||||
|
?.host
|
||||||
|
?.let { nonNullHost ->
|
||||||
|
parseDomainNameOrNullInternal(
|
||||||
|
context = context,
|
||||||
|
host = nonNullHost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal implementation of [parseDomainNameOrNull]. This doesn't extend URI and has a
|
||||||
|
* non-null [host] parameter. Technically, URI.host could be null and we want to avoid issues with
|
||||||
|
* that.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
private fun parseDomainNameOrNullInternal(
|
||||||
|
context: Context,
|
||||||
|
host: String,
|
||||||
|
): DomainName? {
|
||||||
|
val exceptionSuffixes = context
|
||||||
|
.resources
|
||||||
|
.getStringArray(R.array.exception_suffixes)
|
||||||
|
.toList()
|
||||||
|
val normalSuffixes = context
|
||||||
|
.resources
|
||||||
|
.getStringArray(R.array.normal_suffixes)
|
||||||
|
.toList()
|
||||||
|
val wildCardSuffixes = context
|
||||||
|
.resources
|
||||||
|
.getStringArray(R.array.wild_card_suffixes)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
// Split the host into parts separated by a period. Start with the last part and incrementally
|
||||||
|
// add back the earlier parts to build a list of any matching domains in the data set.
|
||||||
|
val hostParts = host
|
||||||
|
.split(".")
|
||||||
|
.reversed()
|
||||||
|
var partialDomain = ""
|
||||||
|
val ruleMatches: MutableList<SuffixMatchType> = mutableListOf()
|
||||||
|
|
||||||
|
// Check to see if this part of the host belongs to any of the suffix lists.
|
||||||
|
hostParts
|
||||||
|
.forEach { hostPart ->
|
||||||
|
partialDomain = if (partialDomain.isBlank()) {
|
||||||
|
hostPart
|
||||||
|
} else {
|
||||||
|
"$hostPart.$partialDomain"
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
// Normal suffixes first.
|
||||||
|
normalSuffixes.contains(partialDomain) -> {
|
||||||
|
ruleMatches.add(
|
||||||
|
SuffixMatchType.Normal(
|
||||||
|
partialDomain = partialDomain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then wild cards.
|
||||||
|
wildCardSuffixes.contains(partialDomain) -> {
|
||||||
|
ruleMatches.add(
|
||||||
|
SuffixMatchType.WildCard(
|
||||||
|
partialDomain = partialDomain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally, exceptions.
|
||||||
|
exceptionSuffixes.contains(partialDomain) -> {
|
||||||
|
ruleMatches.add(
|
||||||
|
SuffixMatchType.Exception(
|
||||||
|
partialDomain = partialDomain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take only the largest public suffix match that occurs within our URI's host. We want the
|
||||||
|
// largest because if the URI was "airbnb.co.uk" we our list would contain "uk" and "co.uk",
|
||||||
|
// which are both valid top level domains. In this case, "uk" is just simply not the top level
|
||||||
|
// domain.
|
||||||
|
val largestMatch = ruleMatches.maxByOrNull {
|
||||||
|
it
|
||||||
|
.partialDomain
|
||||||
|
.split('.')
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the position of the top level domain within the host.
|
||||||
|
val tldRange: IntRange? = when (largestMatch) {
|
||||||
|
is SuffixMatchType.Exception,
|
||||||
|
is SuffixMatchType.Normal,
|
||||||
|
-> {
|
||||||
|
host.findLastSubstringIndicesOrNull(largestMatch.partialDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SuffixMatchType.WildCard -> {
|
||||||
|
// This gets the last portion of the top level domain.
|
||||||
|
val nonWildcardTldIndex = host.lastIndexOf(".${largestMatch.partialDomain}")
|
||||||
|
|
||||||
|
if (nonWildcardTldIndex != -1) {
|
||||||
|
val nonWildcardTld = host.substring(0, nonWildcardTldIndex)
|
||||||
|
|
||||||
|
// But we need to also match the wildcard portion.
|
||||||
|
val dotIndex = nonWildcardTld.lastIndexOf(".")
|
||||||
|
|
||||||
|
if (dotIndex != -1) {
|
||||||
|
IntRange(dotIndex + 1, nonWildcardTldIndex - 1)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return tldRange
|
||||||
|
?.first
|
||||||
|
?.let { firstIndex ->
|
||||||
|
val topLevelDomain = host.substring(firstIndex)
|
||||||
|
|
||||||
|
// Parse the remaining parts prior to the TLD.
|
||||||
|
// - If there's 0 parts left, there is just a TLD and no domain or subdomain.
|
||||||
|
// - If there's 1 part, it's the domain, and there is no subdomain.
|
||||||
|
// - If there's 2+ parts, the last part is the domain, the other parts (combined) are
|
||||||
|
// the subdomain.
|
||||||
|
val possibleSubDomainAndDomain = if (firstIndex > 0) {
|
||||||
|
host.substring(0, firstIndex - 1)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val subDomainAndDomainParts = possibleSubDomainAndDomain?.split(".")
|
||||||
|
val secondLevelDomain = subDomainAndDomainParts?.lastOrNull()
|
||||||
|
val subDomain = subDomainAndDomainParts
|
||||||
|
?.dropLast(1)
|
||||||
|
?.joinToString(separator = ".")
|
||||||
|
// joinToString leaves white space if called on an empty list.
|
||||||
|
// So only take the string if it wasn't empty after the dropLast(1).
|
||||||
|
.takeIf { (subDomainAndDomainParts?.size ?: 0) > 1 }
|
||||||
|
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = secondLevelDomain,
|
||||||
|
topLevelDomain = topLevelDomain,
|
||||||
|
subDomain = subDomain,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of domain suffix match.
|
||||||
|
*/
|
||||||
|
private sealed class SuffixMatchType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The partial domain that was actually matched.
|
||||||
|
*/
|
||||||
|
abstract val partialDomain: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match occurred with an exception suffix that starts with '!'.
|
||||||
|
*/
|
||||||
|
data class Exception(
|
||||||
|
override val partialDomain: String,
|
||||||
|
) : SuffixMatchType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match occurred with a normal suffix.
|
||||||
|
*/
|
||||||
|
data class Normal(
|
||||||
|
override val partialDomain: String,
|
||||||
|
) : SuffixMatchType()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match occurred with a wildcard suffix that starts with '*'.
|
||||||
|
*/
|
||||||
|
data class WildCard(
|
||||||
|
override val partialDomain: String,
|
||||||
|
) : SuffixMatchType()
|
||||||
|
}
|
|
@ -18,3 +18,16 @@ val UriMatchType.displayLabel: Text
|
||||||
UriMatchType.NEVER -> R.string.never
|
UriMatchType.NEVER -> R.string.never
|
||||||
}
|
}
|
||||||
.asText()
|
.asText()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this internal [UriMatchType] to the sdk model.
|
||||||
|
*/
|
||||||
|
fun UriMatchType.toSdkUriMatchType(): com.bitwarden.core.UriMatchType =
|
||||||
|
when (this) {
|
||||||
|
UriMatchType.DOMAIN -> com.bitwarden.core.UriMatchType.DOMAIN
|
||||||
|
UriMatchType.EXACT -> com.bitwarden.core.UriMatchType.EXACT
|
||||||
|
UriMatchType.HOST -> com.bitwarden.core.UriMatchType.HOST
|
||||||
|
UriMatchType.NEVER -> com.bitwarden.core.UriMatchType.NEVER
|
||||||
|
UriMatchType.REGULAR_EXPRESSION -> com.bitwarden.core.UriMatchType.REGULAR_EXPRESSION
|
||||||
|
UriMatchType.STARTS_WITH -> com.bitwarden.core.UriMatchType.STARTS_WITH
|
||||||
|
}
|
||||||
|
|
9576
app/src/main/res/values/public_suffix_list.xml
Normal file
9576
app/src/main/res/values/public_suffix_list.xml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -8,11 +8,13 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
|
||||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||||
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
|
|
||||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
@ -37,6 +39,7 @@ class AutofillCipherProviderTest {
|
||||||
}
|
}
|
||||||
private val cardCipherView: CipherView = mockk {
|
private val cardCipherView: CipherView = mockk {
|
||||||
every { card } returns cardView
|
every { card } returns cardView
|
||||||
|
every { deletedDate } returns null
|
||||||
every { name } returns CARD_NAME
|
every { name } returns CARD_NAME
|
||||||
every { type } returns CipherType.CARD
|
every { type } returns CipherType.CARD
|
||||||
}
|
}
|
||||||
|
@ -45,6 +48,7 @@ class AutofillCipherProviderTest {
|
||||||
every { username } returns LOGIN_USERNAME
|
every { username } returns LOGIN_USERNAME
|
||||||
}
|
}
|
||||||
private val loginCipherView: CipherView = mockk {
|
private val loginCipherView: CipherView = mockk {
|
||||||
|
every { deletedDate } returns null
|
||||||
every { login } returns loginView
|
every { login } returns loginView
|
||||||
every { name } returns LOGIN_NAME
|
every { name } returns LOGIN_NAME
|
||||||
every { type } returns CipherType.LOGIN
|
every { type } returns CipherType.LOGIN
|
||||||
|
@ -52,6 +56,7 @@ class AutofillCipherProviderTest {
|
||||||
private val authRepository: AuthRepository = mockk {
|
private val authRepository: AuthRepository = mockk {
|
||||||
every { activeUserId } returns ACTIVE_USER_ID
|
every { activeUserId } returns ACTIVE_USER_ID
|
||||||
}
|
}
|
||||||
|
private val cipherMatchingManager: CipherMatchingManager = mockk()
|
||||||
private val mutableVaultStateFlow = MutableStateFlow(
|
private val mutableVaultStateFlow = MutableStateFlow(
|
||||||
VaultState(
|
VaultState(
|
||||||
unlockingVaultUserIds = emptySet(),
|
unlockingVaultUserIds = emptySet(),
|
||||||
|
@ -73,17 +78,16 @@ class AutofillCipherProviderTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
mockkStatic(CipherView::takeIfUriMatches)
|
|
||||||
mockkStatic(CipherView::subtitle)
|
mockkStatic(CipherView::subtitle)
|
||||||
autofillCipherProvider = AutofillCipherProviderImpl(
|
autofillCipherProvider = AutofillCipherProviderImpl(
|
||||||
authRepository = authRepository,
|
authRepository = authRepository,
|
||||||
|
cipherMatchingManager = cipherMatchingManager,
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
unmockkStatic(CipherView::takeIfUriMatches)
|
|
||||||
unmockkStatic(CipherView::subtitle)
|
unmockkStatic(CipherView::subtitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,11 +134,17 @@ class AutofillCipherProviderTest {
|
||||||
assertFalse(result.await())
|
assertFalse(result.await())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `getCardAutofillCiphers when unlocked should return non-null card ciphers`() =
|
fun `getCardAutofillCiphers when unlocked should return non-null and non-deleted card ciphers`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
val deletedCardCipherView: CipherView = mockk {
|
||||||
|
every { deletedDate } returns mockk()
|
||||||
|
every { type } returns CipherType.CARD
|
||||||
|
}
|
||||||
val cipherViews = listOf(
|
val cipherViews = listOf(
|
||||||
cardCipherView,
|
cardCipherView,
|
||||||
|
deletedCardCipherView,
|
||||||
loginCipherView,
|
loginCipherView,
|
||||||
)
|
)
|
||||||
mutableCiphersStateFlow.value = DataState.Loaded(
|
mutableCiphersStateFlow.value = DataState.Loaded(
|
||||||
|
@ -168,18 +178,28 @@ class AutofillCipherProviderTest {
|
||||||
assertEquals(emptyList<AutofillCipher.Card>(), actual)
|
assertEquals(emptyList<AutofillCipher.Card>(), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `getLoginAutofillCiphers when unlocked should return matching login ciphers`() =
|
fun `getLoginAutofillCiphers when unlocked should return matched, non-deleted, login ciphers`() =
|
||||||
runTest {
|
runTest {
|
||||||
val unmatchedLoginCipherView: CipherView = mockk {
|
val deletedLoginCipherView: CipherView = mockk {
|
||||||
every { takeIfUriMatches(URI) } returns null
|
every { deletedDate } returns mockk()
|
||||||
every { type } returns CipherType.LOGIN
|
every { type } returns CipherType.LOGIN
|
||||||
}
|
}
|
||||||
val cipherViews = listOf(
|
val cipherViews = listOf(
|
||||||
cardCipherView,
|
cardCipherView,
|
||||||
loginCipherView,
|
loginCipherView,
|
||||||
unmatchedLoginCipherView,
|
deletedLoginCipherView,
|
||||||
)
|
)
|
||||||
|
val filteredCipherViews = listOf(
|
||||||
|
loginCipherView,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = filteredCipherViews,
|
||||||
|
matchUri = URI,
|
||||||
|
)
|
||||||
|
} returns filteredCipherViews
|
||||||
mutableCiphersStateFlow.value = DataState.Loaded(
|
mutableCiphersStateFlow.value = DataState.Loaded(
|
||||||
data = cipherViews,
|
data = cipherViews,
|
||||||
)
|
)
|
||||||
|
@ -191,14 +211,20 @@ class AutofillCipherProviderTest {
|
||||||
LOGIN_AUTOFILL_CIPHER,
|
LOGIN_AUTOFILL_CIPHER,
|
||||||
)
|
)
|
||||||
every { loginCipherView.subtitle } returns LOGIN_SUBTITLE
|
every { loginCipherView.subtitle } returns LOGIN_SUBTITLE
|
||||||
every { loginCipherView.takeIfUriMatches(URI) } returns loginCipherView
|
|
||||||
|
|
||||||
// Test & Verify
|
// Test
|
||||||
val actual = autofillCipherProvider.getLoginAutofillCiphers(
|
val actual = autofillCipherProvider.getLoginAutofillCiphers(
|
||||||
uri = URI,
|
uri = URI,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
|
coVerify {
|
||||||
|
cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = filteredCipherViews,
|
||||||
|
matchUri = URI,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -0,0 +1,385 @@
|
||||||
|
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bitwarden.core.CipherView
|
||||||
|
import com.bitwarden.core.LoginUriView
|
||||||
|
import com.bitwarden.core.LoginView
|
||||||
|
import com.bitwarden.core.UriMatchType
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getHostWithPortOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class CipherMatchingManagerTest {
|
||||||
|
private lateinit var cipherMatchingManager: CipherMatchingManager
|
||||||
|
|
||||||
|
// Setup dependencies
|
||||||
|
private val context: Context = mockk()
|
||||||
|
private val settingsRepository: SettingsRepository = mockk {
|
||||||
|
every { defaultUriMatchType } returns DEFAULT_URI_MATCH_TYPE
|
||||||
|
}
|
||||||
|
private val vaultRepository: VaultRepository = mockk {
|
||||||
|
every { domainsStateFlow } returns MutableStateFlow(DataState.Loaded(DOMAINS_DATA))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup test ciphers
|
||||||
|
private val defaultMatchLoginUriViewOne: LoginUriView = mockk {
|
||||||
|
every { match } returns null
|
||||||
|
every { uri } returns DEFAULT_LOGIN_VIEW_URI_ONE
|
||||||
|
}
|
||||||
|
private val defaultMatchLoginUriViewTwo: LoginUriView = mockk {
|
||||||
|
every { match } returns null
|
||||||
|
every { uri } returns DEFAULT_LOGIN_VIEW_URI_TWO
|
||||||
|
}
|
||||||
|
private val defaultMatchLoginUriViewThree: LoginUriView = mockk {
|
||||||
|
every { match } returns null
|
||||||
|
every { uri } returns DEFAULT_LOGIN_VIEW_URI_THREE
|
||||||
|
}
|
||||||
|
private val defaultMatchLoginUriViewFour: LoginUriView = mockk {
|
||||||
|
every { match } returns null
|
||||||
|
every { uri } returns DEFAULT_LOGIN_VIEW_URI_FOUR
|
||||||
|
}
|
||||||
|
private val defaultMatchLoginUriViewFive: LoginUriView = mockk {
|
||||||
|
every { match } returns null
|
||||||
|
every { uri } returns DEFAULT_LOGIN_VIEW_URI_FIVE
|
||||||
|
}
|
||||||
|
private val defaultMatchLoginView: LoginView = mockk {
|
||||||
|
every { uris } returns listOf(
|
||||||
|
defaultMatchLoginUriViewOne,
|
||||||
|
defaultMatchLoginUriViewTwo,
|
||||||
|
defaultMatchLoginUriViewThree,
|
||||||
|
defaultMatchLoginUriViewFour,
|
||||||
|
defaultMatchLoginUriViewFive,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val defaultMatchCipher: CipherView = mockk {
|
||||||
|
every { login } returns defaultMatchLoginView
|
||||||
|
}
|
||||||
|
private val exactMatchLoginUriViewOne: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.EXACT
|
||||||
|
every { uri } returns "google.com"
|
||||||
|
}
|
||||||
|
private val exactMatchLoginUriViewTwo: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.EXACT
|
||||||
|
every { uri } returns "notExactMatch.com"
|
||||||
|
}
|
||||||
|
private val exactMatchLoginView: LoginView = mockk {
|
||||||
|
every { uris } returns listOf(
|
||||||
|
exactMatchLoginUriViewOne,
|
||||||
|
exactMatchLoginUriViewTwo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val exactMatchCipher: CipherView = mockk {
|
||||||
|
every { login } returns exactMatchLoginView
|
||||||
|
}
|
||||||
|
private val hostMatchLoginUriViewMatching: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.HOST
|
||||||
|
every { uri } returns HOST_LOGIN_VIEW_URI_MATCHING
|
||||||
|
}
|
||||||
|
private val hostMatchLoginUriViewNotMatching: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.HOST
|
||||||
|
every { uri } returns HOST_LOGIN_VIEW_URI_NOT_MATCHING
|
||||||
|
}
|
||||||
|
private val hostMatchLoginView: LoginView = mockk {
|
||||||
|
every { uris } returns listOf(
|
||||||
|
hostMatchLoginUriViewMatching,
|
||||||
|
hostMatchLoginUriViewNotMatching,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val hostMatchCipher: CipherView = mockk {
|
||||||
|
every { login } returns hostMatchLoginView
|
||||||
|
}
|
||||||
|
private val neverMatchLoginUriView: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.NEVER
|
||||||
|
every { uri } returns "google.com"
|
||||||
|
}
|
||||||
|
private val neverMatchLoginView: LoginView = mockk {
|
||||||
|
every { uris } returns listOf(neverMatchLoginUriView)
|
||||||
|
}
|
||||||
|
private val neverMatchCipher: CipherView = mockk {
|
||||||
|
every { login } returns neverMatchLoginView
|
||||||
|
}
|
||||||
|
private val regexMatchLoginUriViewMatching: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.REGULAR_EXPRESSION
|
||||||
|
every { uri } returns ".*"
|
||||||
|
}
|
||||||
|
private val regexMatchLoginUriViewNotMatching: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.REGULAR_EXPRESSION
|
||||||
|
every { uri } returns "$^"
|
||||||
|
}
|
||||||
|
private val regexMatchLoginView: LoginView = mockk {
|
||||||
|
every { uris } returns listOf(
|
||||||
|
regexMatchLoginUriViewMatching,
|
||||||
|
regexMatchLoginUriViewNotMatching,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val regexMatchCipher: CipherView = mockk {
|
||||||
|
every { login } returns regexMatchLoginView
|
||||||
|
}
|
||||||
|
private val startsWithMatchLoginUriViewMatching: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.STARTS_WITH
|
||||||
|
every { uri } returns "g"
|
||||||
|
}
|
||||||
|
private val startsWithMatchLoginUriViewNotMatching: LoginUriView = mockk {
|
||||||
|
every { match } returns UriMatchType.REGULAR_EXPRESSION
|
||||||
|
every { uri } returns "!!!!!!"
|
||||||
|
}
|
||||||
|
private val startsWithMatchLoginView: LoginView = mockk {
|
||||||
|
every { uris } returns listOf(
|
||||||
|
startsWithMatchLoginUriViewMatching,
|
||||||
|
startsWithMatchLoginUriViewNotMatching,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val startsWithMatchCipher: CipherView = mockk {
|
||||||
|
every { login } returns startsWithMatchLoginView
|
||||||
|
}
|
||||||
|
private val ciphers: List<CipherView> = listOf(
|
||||||
|
defaultMatchCipher,
|
||||||
|
exactMatchCipher,
|
||||||
|
hostMatchCipher,
|
||||||
|
neverMatchCipher,
|
||||||
|
regexMatchCipher,
|
||||||
|
startsWithMatchCipher,
|
||||||
|
)
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic(
|
||||||
|
String::isAndroidApp,
|
||||||
|
String::getDomainOrNull,
|
||||||
|
String::getWebHostFromAndroidUriOrNull,
|
||||||
|
)
|
||||||
|
cipherMatchingManager = CipherMatchingManagerImpl(
|
||||||
|
context = context,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
vaultRepository = vaultRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun teardown() {
|
||||||
|
unmockkStatic(
|
||||||
|
String::isAndroidApp,
|
||||||
|
String::getDomainOrNull,
|
||||||
|
String::getWebHostFromAndroidUriOrNull,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `filterCiphersForMatches should perform cipher matching when is android app and matching URI`() =
|
||||||
|
runTest {
|
||||||
|
// Setup
|
||||||
|
val uri = "google.com"
|
||||||
|
val expected = listOf(
|
||||||
|
defaultMatchCipher,
|
||||||
|
exactMatchCipher,
|
||||||
|
hostMatchCipher,
|
||||||
|
regexMatchCipher,
|
||||||
|
startsWithMatchCipher,
|
||||||
|
)
|
||||||
|
setupMocksForMatchingCiphers(
|
||||||
|
isAndroidApp = true,
|
||||||
|
uri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = ciphers,
|
||||||
|
matchUri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `filterCiphersForMatches should perform cipher matching when is android app and difficult to match URI`() =
|
||||||
|
runTest {
|
||||||
|
// Setup
|
||||||
|
val uri = "difficultToMatch.com"
|
||||||
|
// The default cipher only has a fuzzy match
|
||||||
|
// and therefore is at the end of the list.
|
||||||
|
val expected = listOf(
|
||||||
|
hostMatchCipher,
|
||||||
|
regexMatchCipher,
|
||||||
|
defaultMatchCipher,
|
||||||
|
)
|
||||||
|
setupMocksForMatchingCiphers(
|
||||||
|
isAndroidApp = true,
|
||||||
|
uri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = ciphers,
|
||||||
|
matchUri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `filterCiphersForMatches should perform cipher matching when not android app and matching URI`() =
|
||||||
|
runTest {
|
||||||
|
// Setup
|
||||||
|
val uri = "google.com"
|
||||||
|
val expected = listOf(
|
||||||
|
defaultMatchCipher,
|
||||||
|
exactMatchCipher,
|
||||||
|
hostMatchCipher,
|
||||||
|
regexMatchCipher,
|
||||||
|
startsWithMatchCipher,
|
||||||
|
)
|
||||||
|
setupMocksForMatchingCiphers(
|
||||||
|
isAndroidApp = false,
|
||||||
|
uri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = ciphers,
|
||||||
|
matchUri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `filterCiphersForMatches should perform cipher matching when not android app and difficult to match URI`() =
|
||||||
|
runTest {
|
||||||
|
// Setup
|
||||||
|
val uri = "difficultToMatch.com"
|
||||||
|
val expected = listOf(
|
||||||
|
hostMatchCipher,
|
||||||
|
regexMatchCipher,
|
||||||
|
)
|
||||||
|
setupMocksForMatchingCiphers(
|
||||||
|
isAndroidApp = false,
|
||||||
|
uri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = ciphers,
|
||||||
|
matchUri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `filterCiphersForMatches should skip ciphers without login details`() =
|
||||||
|
runTest {
|
||||||
|
// Setup
|
||||||
|
val uri = "noMatches.com"
|
||||||
|
val ciphers = listOf<CipherView>(
|
||||||
|
mockk {
|
||||||
|
every { login } returns null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with(uri) {
|
||||||
|
every { isAndroidApp() } returns false
|
||||||
|
every { getDomainOrNull(context = context) } returns this
|
||||||
|
every { getWebHostFromAndroidUriOrNull() } returns null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = cipherMatchingManager.filterCiphersForMatches(
|
||||||
|
ciphers = ciphers,
|
||||||
|
matchUri = uri,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(emptyList<CipherView>(), actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup mocks for matching the massive list of [ciphers].
|
||||||
|
*/
|
||||||
|
private fun setupMocksForMatchingCiphers(
|
||||||
|
isAndroidApp: Boolean,
|
||||||
|
uri: String,
|
||||||
|
) {
|
||||||
|
with(uri) {
|
||||||
|
every { isAndroidApp() } returns isAndroidApp
|
||||||
|
every { getDomainOrNull(context = context) } returns this.takeIf { isAndroidApp }
|
||||||
|
every { getHostWithPortOrNull() } returns HOST_WITH_PORT
|
||||||
|
every {
|
||||||
|
getWebHostFromAndroidUriOrNull()
|
||||||
|
} returns ANDROID_APP_WEB_URL.takeIf { isAndroidApp }
|
||||||
|
}
|
||||||
|
every {
|
||||||
|
DEFAULT_LOGIN_VIEW_URI_ONE.getDomainOrNull(context = context)
|
||||||
|
} returns DEFAULT_LOGIN_VIEW_URI_ONE
|
||||||
|
every {
|
||||||
|
DEFAULT_LOGIN_VIEW_URI_TWO.getDomainOrNull(context = context)
|
||||||
|
} returns null
|
||||||
|
every {
|
||||||
|
DEFAULT_LOGIN_VIEW_URI_THREE.getDomainOrNull(context = context)
|
||||||
|
} returns uri
|
||||||
|
every {
|
||||||
|
DEFAULT_LOGIN_VIEW_URI_FOUR.getDomainOrNull(context = context)
|
||||||
|
} returns "bitwarden.com"
|
||||||
|
every {
|
||||||
|
DEFAULT_LOGIN_VIEW_URI_FIVE.getDomainOrNull(context = context)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
every { HOST_LOGIN_VIEW_URI_MATCHING.getHostWithPortOrNull() } returns HOST_WITH_PORT
|
||||||
|
every { HOST_LOGIN_VIEW_URI_NOT_MATCHING.getHostWithPortOrNull() } returns null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val ANDROID_APP_WEB_URL = "ANDROID_APP_WEB_URL"
|
||||||
|
private val DEFAULT_URI_MATCH_TYPE =
|
||||||
|
com.x8bit.bitwarden.data.platform.repository.model.UriMatchType.DOMAIN
|
||||||
|
private val EQUIVALENT_DOMAINS = listOf(
|
||||||
|
"google.com",
|
||||||
|
"google.co.uk",
|
||||||
|
)
|
||||||
|
private val GLOBAL_EQUIVALENT_DOMAINS_DATA = listOf(
|
||||||
|
"bitwarden.com",
|
||||||
|
"bitwarden.co.uk",
|
||||||
|
ANDROID_APP_WEB_URL,
|
||||||
|
)
|
||||||
|
private val GLOBAL_EQUIVALENT_DOMAINS = DomainsData.GlobalEquivalentDomain(
|
||||||
|
isExcluded = false,
|
||||||
|
domains = GLOBAL_EQUIVALENT_DOMAINS_DATA,
|
||||||
|
type = 0,
|
||||||
|
)
|
||||||
|
private val DOMAINS_DATA = DomainsData(
|
||||||
|
equivalentDomains = listOf(EQUIVALENT_DOMAINS),
|
||||||
|
globalEquivalentDomains = listOf(GLOBAL_EQUIVALENT_DOMAINS),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup state for default ciphers
|
||||||
|
private const val DEFAULT_LOGIN_VIEW_URI_ONE: String = "google.com"
|
||||||
|
private const val DEFAULT_LOGIN_VIEW_URI_TWO: String = ANDROID_APP_WEB_URL
|
||||||
|
private const val DEFAULT_LOGIN_VIEW_URI_THREE: String = "DEFAULT_LOGIN_VIEW_URI_THREE"
|
||||||
|
private const val DEFAULT_LOGIN_VIEW_URI_FOUR: String = "DEFAULT_LOGIN_VIEW_URI_FOUR"
|
||||||
|
private const val DEFAULT_LOGIN_VIEW_URI_FIVE: String = "DEFAULT_LOGIN_VIEW_URI_FIVE"
|
||||||
|
|
||||||
|
// Setup state for host ciphers
|
||||||
|
private const val HOST_LOGIN_VIEW_URI_MATCHING: String = "DEFAULT_LOGIN_VIEW_URI_MATCHING"
|
||||||
|
private const val HOST_LOGIN_VIEW_URI_NOT_MATCHING: String = "DEFAULT_LOGIN_VIEW_URI_NOT_MATCHING"
|
||||||
|
private const val HOST_WITH_PORT: String = "HOST_WITH_PORT"
|
|
@ -4,10 +4,7 @@ import com.bitwarden.core.CardView
|
||||||
import com.bitwarden.core.CipherType
|
import com.bitwarden.core.CipherType
|
||||||
import com.bitwarden.core.CipherView
|
import com.bitwarden.core.CipherView
|
||||||
import com.bitwarden.core.IdentityView
|
import com.bitwarden.core.IdentityView
|
||||||
import com.bitwarden.core.LoginUriView
|
|
||||||
import com.bitwarden.core.LoginView
|
|
||||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||||
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
@ -332,52 +329,4 @@ class CipherViewExtensionsTest {
|
||||||
// Verify
|
// Verify
|
||||||
assertNull(actual)
|
assertNull(actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `takeIfUriMatches should return the CipherView if the URI matches`() {
|
|
||||||
// Setup
|
|
||||||
val testUri = "com.x8bit.bitwarden"
|
|
||||||
val loginUriView: LoginUriView = mockk {
|
|
||||||
every { uri } returns testUri
|
|
||||||
}
|
|
||||||
val loginView: LoginView = mockk {
|
|
||||||
every { uris } returns listOf(loginUriView)
|
|
||||||
}
|
|
||||||
val expected: CipherView = mockk {
|
|
||||||
every { login } returns loginView
|
|
||||||
every { type } returns CipherType.IDENTITY
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test
|
|
||||||
val actual = expected.takeIfUriMatches(
|
|
||||||
uri = testUri,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
assertEquals(expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `takeIfUriMatches should return null if the URI doesn't match`() {
|
|
||||||
// Setup
|
|
||||||
val testUri = "com.x8bit.bitwarden"
|
|
||||||
val loginUriView: LoginUriView = mockk {
|
|
||||||
every { uri } returns "com.google"
|
|
||||||
}
|
|
||||||
val loginView: LoginView = mockk {
|
|
||||||
every { uris } returns listOf(loginUriView)
|
|
||||||
}
|
|
||||||
val cipherView: CipherView = mockk {
|
|
||||||
every { login } returns loginView
|
|
||||||
every { type } returns CipherType.IDENTITY
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test
|
|
||||||
val actual = cipherView.takeIfUriMatches(
|
|
||||||
uri = testUri,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
assertNull(actual)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
package com.x8bit.bitwarden.data.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.findLastSubstringIndicesOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.parseDomainOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.toUriOrNull
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class StringExtensionsTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun teardown() {
|
||||||
|
unmockkStatic(URI::parseDomainOrNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toUriOrNull should return null when uri is malformed`() {
|
||||||
|
assertNull("not a uri".toUriOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toUriOrNull should return URI when uri is valid`() {
|
||||||
|
assertNotNull("www.google.com".toUriOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isAndroidApp should return true when string starts with android app protocol`() {
|
||||||
|
assertTrue("androidapp://com.x8bit.bitwarden".isAndroidApp())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `isAndroidApp should return false when doesn't start with android app protocol`() {
|
||||||
|
assertFalse("com.x8bit.bitwarden".isAndroidApp())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getWebHostFromAndroidUriOrNull should return null when not android app`() {
|
||||||
|
assertNull("com.x8bit.bitwarden".getWebHostFromAndroidUriOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getWebHostFromAndroidUriOrNull should return null when no dot`() {
|
||||||
|
assertNull("androidapp://comx8bitbitwarden".getWebHostFromAndroidUriOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getWebHostFromAndroidUriOrNull should return web host when has dot`() {
|
||||||
|
// Setup
|
||||||
|
val expected = "x8bit.com"
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = "androidapp://com.x8bit.bitwarden".getWebHostFromAndroidUriOrNull()
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getDomainOrNull should invoke parseDomainOrNull when URI is created`() {
|
||||||
|
// Setup
|
||||||
|
mockkStatic(URI::parseDomainOrNull)
|
||||||
|
val context: Context = mockk()
|
||||||
|
val expected = "google.com"
|
||||||
|
every {
|
||||||
|
any<URI>().parseDomainOrNull(context = context)
|
||||||
|
} returns expected
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = "www.google.com".getDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
any<URI>().parseDomainOrNull(context = context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getDomainOrNull should not invoke parseDomainOrNull when URI is not created`() {
|
||||||
|
// Setup
|
||||||
|
mockkStatic(URI::parseDomainOrNull)
|
||||||
|
val context: Context = mockk()
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = "not a URI".getDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(actual)
|
||||||
|
verify(exactly = 0) {
|
||||||
|
any<URI>().parseDomainOrNull(context = context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `findLastSubstringIndicesOrNull should return null if substring doesn't appear`() {
|
||||||
|
// Setup
|
||||||
|
val substring = "hello"
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = "goodbye".findLastSubstringIndicesOrNull(
|
||||||
|
substring = substring,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `findLastSubstringIndicesOrNull should return indices of last substring appearance`() {
|
||||||
|
// Setup
|
||||||
|
val substring = "hello"
|
||||||
|
val expected = IntRange(7, 11)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = "hello, hello".findLastSubstringIndicesOrNull(
|
||||||
|
substring = substring,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
package com.x8bit.bitwarden.data.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.model.DomainName
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.parseDomainNameOrNull
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.parseDomainOrNull
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class UriExtensionsTest {
|
||||||
|
private val resources: Resources = mockk {
|
||||||
|
every { getStringArray(R.array.exception_suffixes) } returns emptyArray()
|
||||||
|
every { getStringArray(R.array.normal_suffixes) } returns emptyArray()
|
||||||
|
every { getStringArray(R.array.wild_card_suffixes) } returns emptyArray()
|
||||||
|
}
|
||||||
|
private val context: Context = mockk {
|
||||||
|
every { this@mockk.resources } returns this@UriExtensionsTest.resources
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainOrNull should return null when host is null`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk {
|
||||||
|
every { host } returns null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainOrNull should return host when it is localhost`() {
|
||||||
|
// Setup
|
||||||
|
val expected = "localhost"
|
||||||
|
val uri: URI = mockk {
|
||||||
|
every { host } returns expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainOrNull should return host when it is ip address`() {
|
||||||
|
// Setup
|
||||||
|
val expected = "192.168.1.1"
|
||||||
|
val uri: URI = mockk {
|
||||||
|
every { host } returns expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainOrNull should return domain name when it matches exception suffix`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk()
|
||||||
|
val expected = listOf(
|
||||||
|
"example.co.uk",
|
||||||
|
"example.co.uk",
|
||||||
|
"example.uk",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
every {
|
||||||
|
resources.getStringArray(R.array.exception_suffixes)
|
||||||
|
} returns arrayOf("co.uk", "uk")
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
listOf(
|
||||||
|
"example.co.uk",
|
||||||
|
"sub.example.co.uk",
|
||||||
|
"sub.example.uk",
|
||||||
|
"sub.example.uk.usa",
|
||||||
|
)
|
||||||
|
.forEachIndexed { index, host ->
|
||||||
|
// Setup item test
|
||||||
|
every { uri.host } returns host
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected[index], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainOrNull should return domain name when it matches normal suffix`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk()
|
||||||
|
val expected = listOf(
|
||||||
|
"example.co.uk",
|
||||||
|
"example.co.uk",
|
||||||
|
"example.uk",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
every { resources.getStringArray(R.array.exception_suffixes) } returns arrayOf(
|
||||||
|
"co.uk",
|
||||||
|
"uk",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
listOf(
|
||||||
|
"example.co.uk",
|
||||||
|
"sub.example.co.uk",
|
||||||
|
"sub.example.uk",
|
||||||
|
"sub.example.uk.usa",
|
||||||
|
)
|
||||||
|
.forEachIndexed { index, host ->
|
||||||
|
// Setup item test
|
||||||
|
every { uri.host } returns host
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected[index], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainOrNull should return domain name when it matches wild card suffix`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk()
|
||||||
|
val expected = listOf(
|
||||||
|
"example.foo.compute.amazonaws.com",
|
||||||
|
"example.foo.compute.amazonaws.com",
|
||||||
|
"example.foo.amazonaws.com",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
every { resources.getStringArray(R.array.wild_card_suffixes) } returns arrayOf(
|
||||||
|
"compute.amazonaws.com",
|
||||||
|
"amazonaws.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
listOf(
|
||||||
|
"sub.example.foo.compute.amazonaws.com",
|
||||||
|
"bar.sub.example.foo.compute.amazonaws.com",
|
||||||
|
"bar.sub.example.foo.amazonaws.com",
|
||||||
|
"foo.sub.example.foo.amazonaws.com.usa",
|
||||||
|
)
|
||||||
|
.forEachIndexed { index, host ->
|
||||||
|
// Setup item test
|
||||||
|
every { uri.host } returns host
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected[index], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainNameOrNull should return null when host is null`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk {
|
||||||
|
every { host } returns null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainNameOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertNull(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainNameOrNull should return domain name when it matches exception suffix`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk()
|
||||||
|
val expected = listOf(
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = null,
|
||||||
|
topLevelDomain = "co.uk",
|
||||||
|
),
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "sub",
|
||||||
|
topLevelDomain = "co.uk",
|
||||||
|
),
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "sub",
|
||||||
|
topLevelDomain = "uk",
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
every { resources.getStringArray(R.array.exception_suffixes) } returns arrayOf(
|
||||||
|
"co.uk",
|
||||||
|
"uk",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
listOf(
|
||||||
|
"example.co.uk",
|
||||||
|
"sub.example.co.uk",
|
||||||
|
"sub.example.uk",
|
||||||
|
"sub.example.uk.usa",
|
||||||
|
)
|
||||||
|
.forEachIndexed { index, host ->
|
||||||
|
// Setup item test
|
||||||
|
every { uri.host } returns host
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainNameOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected[index], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainNameOrNull should return domain name when it matches normal suffix`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk()
|
||||||
|
val expected = listOf(
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = null,
|
||||||
|
topLevelDomain = "co.uk",
|
||||||
|
),
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "sub",
|
||||||
|
topLevelDomain = "co.uk",
|
||||||
|
),
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "sub",
|
||||||
|
topLevelDomain = "uk",
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
every { resources.getStringArray(R.array.exception_suffixes) } returns arrayOf(
|
||||||
|
"co.uk",
|
||||||
|
"uk",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
listOf(
|
||||||
|
"example.co.uk",
|
||||||
|
"sub.example.co.uk",
|
||||||
|
"sub.example.uk",
|
||||||
|
"sub.example.uk.usa",
|
||||||
|
)
|
||||||
|
.forEachIndexed { index, host ->
|
||||||
|
// Setup item test
|
||||||
|
every { uri.host } returns host
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainNameOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected[index], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseDomainNameOrNull should return domain name when it matches wild card suffix`() {
|
||||||
|
// Setup
|
||||||
|
val uri: URI = mockk()
|
||||||
|
val expected = listOf(
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "sub",
|
||||||
|
topLevelDomain = "foo.compute.amazonaws.com",
|
||||||
|
),
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "bar.sub",
|
||||||
|
topLevelDomain = "foo.compute.amazonaws.com",
|
||||||
|
),
|
||||||
|
DomainName(
|
||||||
|
secondLevelDomain = "example",
|
||||||
|
subDomain = "bar.sub",
|
||||||
|
topLevelDomain = "foo.amazonaws.com",
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
every { resources.getStringArray(R.array.wild_card_suffixes) } returns arrayOf(
|
||||||
|
"compute.amazonaws.com",
|
||||||
|
"amazonaws.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test & Verify
|
||||||
|
listOf(
|
||||||
|
"sub.example.foo.compute.amazonaws.com",
|
||||||
|
"bar.sub.example.foo.compute.amazonaws.com",
|
||||||
|
"bar.sub.example.foo.amazonaws.com",
|
||||||
|
"bar.sub.example.foo.amazonaws.com.usa",
|
||||||
|
)
|
||||||
|
.forEachIndexed { index, host ->
|
||||||
|
// Setup item test
|
||||||
|
every { uri.host } returns host
|
||||||
|
|
||||||
|
// Test
|
||||||
|
val actual = uri.parseDomainNameOrNull(
|
||||||
|
context = context,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
assertEquals(expected[index], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,4 +24,32 @@ class UriMatchTypeExtensionsTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toSdkUriMatchType should return the correct value for each type`() {
|
||||||
|
assertEquals(
|
||||||
|
com.bitwarden.core.UriMatchType.DOMAIN,
|
||||||
|
UriMatchType.DOMAIN.toSdkUriMatchType(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
com.bitwarden.core.UriMatchType.EXACT,
|
||||||
|
UriMatchType.EXACT.toSdkUriMatchType(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
com.bitwarden.core.UriMatchType.HOST,
|
||||||
|
UriMatchType.HOST.toSdkUriMatchType(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
com.bitwarden.core.UriMatchType.NEVER,
|
||||||
|
UriMatchType.NEVER.toSdkUriMatchType(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
com.bitwarden.core.UriMatchType.REGULAR_EXPRESSION,
|
||||||
|
UriMatchType.REGULAR_EXPRESSION.toSdkUriMatchType(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
com.bitwarden.core.UriMatchType.STARTS_WITH,
|
||||||
|
UriMatchType.STARTS_WITH.toSdkUriMatchType(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
15411
scripts/public_suffix_list.dat
Normal file
15411
scripts/public_suffix_list.dat
Normal file
File diff suppressed because it is too large
Load diff
76
scripts/public_suffix_list_generator.sh
Normal file
76
scripts/public_suffix_list_generator.sh
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This shell script is for parsing `public_suffix_list.dat` and writing its contents into a string
|
||||||
|
# array resource. We need these public suffixes for extracting a domain name from a URI. For more
|
||||||
|
# information about why that is, checkout the original PR description here:
|
||||||
|
# https://github.com/bitwarden/android/pull/842.
|
||||||
|
#
|
||||||
|
# For an updated `public_suffix_list.dat` file, go here: https://publicsuffix.org/list/index.html
|
||||||
|
|
||||||
|
RAW_PREFIXES_DATA_PATH="public_suffix_list.dat"
|
||||||
|
RESOURCE_OUTPUT_PATH="../app/src/main/res/values/public_suffix_list.xml"
|
||||||
|
EXCEPTION_SUFFIXES_STRING_ARRAY_HEADER=" <string-array name=\"exception_suffixes\">\n"
|
||||||
|
NORMAL_SUFFIXES_STRING_ARRAY_HEADER=" <string-array name=\"normal_suffixes\">\n"
|
||||||
|
WILD_CARD_SUFFIXES_STRING_ARRAY_HEADER=" <string-array name=\"wild_card_suffixes\">\n"
|
||||||
|
STRING_ARRAY_FOOTER=" </string-array>\n"
|
||||||
|
STRING_RESOURCE_FOOTER="</resources>\n"
|
||||||
|
STRING_RESOURCE_HEADER="<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n A list of valid public domain suffixes. This file is generated by running\n \`scripts/public_suffix_list_generator.sh\` \n-->\n<resources>\n"
|
||||||
|
|
||||||
|
# Setup the header of the file.
|
||||||
|
newFileContents=$STRING_RESOURCE_HEADER
|
||||||
|
|
||||||
|
exceptionSuffixes=()
|
||||||
|
normalSuffixes=()
|
||||||
|
wildcardSuffixes=()
|
||||||
|
|
||||||
|
addItemToContents() {
|
||||||
|
newFileContents+=" <item>$1</item>\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
while IFS= read -r string || [[ -n "$string" ]]; do
|
||||||
|
if [[ -z "$string" || "$string" == "//"* ]]; then
|
||||||
|
# Remove comments and empty lines
|
||||||
|
continue
|
||||||
|
elif [[ "$string" == "*"* ]]; then
|
||||||
|
# Drop the "*."
|
||||||
|
wildcardSuffixes+=("${string#??}")
|
||||||
|
elif [[ "$string" == "!"* ]]; then
|
||||||
|
# Drop the "!."
|
||||||
|
exceptionSuffixes+=("${string#?}")
|
||||||
|
else
|
||||||
|
normalSuffixes+=("$string")
|
||||||
|
fi
|
||||||
|
done < "$RAW_PREFIXES_DATA_PATH"
|
||||||
|
|
||||||
|
# Add all exception suffix items to the new file contents.
|
||||||
|
if [ ${#exceptionSuffixes[@]} -gt 0 ]; then
|
||||||
|
newFileContents+=$EXCEPTION_SUFFIXES_STRING_ARRAY_HEADER
|
||||||
|
for item in "${exceptionSuffixes[@]}"; do
|
||||||
|
addItemToContents "$item"
|
||||||
|
done
|
||||||
|
newFileContents+=$STRING_ARRAY_FOOTER
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add all normal suffix items to the new file contents.
|
||||||
|
if [ ${#normalSuffixes[@]} -gt 0 ]; then
|
||||||
|
newFileContents+=$NORMAL_SUFFIXES_STRING_ARRAY_HEADER
|
||||||
|
for item in "${normalSuffixes[@]}"; do
|
||||||
|
addItemToContents "$item"
|
||||||
|
done
|
||||||
|
newFileContents+=$STRING_ARRAY_FOOTER
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add all wild card suffix items to the new file contents.
|
||||||
|
if [ ${#wildcardSuffixes[@]} -gt 0 ]; then
|
||||||
|
newFileContents+=$WILD_CARD_SUFFIXES_STRING_ARRAY_HEADER
|
||||||
|
for item in "${wildcardSuffixes[@]}"; do
|
||||||
|
addItemToContents "$item"
|
||||||
|
done
|
||||||
|
newFileContents+=$STRING_ARRAY_FOOTER
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup the footer of the file.
|
||||||
|
newFileContents+=$STRING_RESOURCE_FOOTER
|
||||||
|
|
||||||
|
# Write the contents to the output file.
|
||||||
|
echo -e "$newFileContents" > "$RESOURCE_OUTPUT_PATH"
|
Loading…
Add table
Reference in a new issue