From 110e2b7580d85b6ef2b7a19033eb019c29b7e5d5 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Thu, 22 Oct 2020 21:16:44 +0200 Subject: [PATCH] Fix subdomain search #728 --- app/src/main/assets/publicsuffixes | Bin 0 -> 107303 bytes .../activities/AutofillLauncherActivity.kt | 160 ++++++++++-------- .../EntrySelectionLauncherActivity.kt | 21 ++- .../keepass/activities/GroupActivity.kt | 3 +- .../keepass/database/search/SearchHelper.kt | 3 +- .../com/kunzisoft/keepass/model/SearchInfo.kt | 14 -- .../com/kunzisoft/keepass/utils/UriUtil.kt | 23 ++- .../lib/publicsuffixlist/PublicSuffixList.kt | 135 +++++++++++++++ .../publicsuffixlist/PublicSuffixListData.kt | 158 +++++++++++++++++ .../PublicSuffixListLoader.kt | 48 ++++++ .../lib/publicsuffixlist/ext/ByteArray.kt | 122 +++++++++++++ 11 files changed, 586 insertions(+), 101 deletions(-) create mode 100644 app/src/main/assets/publicsuffixes create mode 100644 app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt create mode 100644 app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt create mode 100644 app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt create mode 100644 app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt diff --git a/app/src/main/assets/publicsuffixes b/app/src/main/assets/publicsuffixes new file mode 100644 index 0000000000000000000000000000000000000000..13841ad8d8facefd435b70749f430693e9836616 GIT binary patch literal 107303 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 WXf6yi(hT0zj4oJSUaps%%>@7)L}DKR literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt index 09d9e427b..66ab95e6b 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt @@ -40,6 +40,7 @@ import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.LOCK_ACTION +import com.kunzisoft.keepass.utils.UriUtil @RequiresApi(api = Build.VERSION_CODES.O) class AutofillLauncherActivity : AppCompatActivity() { @@ -61,83 +62,26 @@ class AutofillLauncherActivity : AppCompatActivity() { webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) } - // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) - val assistStructure = AutofillHelper.retrieveAssistStructure(intent) - - if (assistStructure == null) { - setResult(Activity.RESULT_CANCELED) - finish() - } else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, - PreferencesUtil.applicationIdBlocklist(this)) - || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, - PreferencesUtil.webDomainBlocklist(this))) { - // If item not allowed, show a toast - Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show() - setResult(Activity.RESULT_CANCELED) - finish() + if (!PreferencesUtil.searchSubdomains(this)) { + UriUtil.getWebDomainWithoutSubDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> + searchInfo.webDomain = webDomainWithoutSubDomain + launchSelection(searchInfo) + } } else { - // If database is open - SearchHelper.checkAutoSearchInfo(this, - Database.getInstance(), - searchInfo, - { items -> - // Items found - AutofillHelper.buildResponse(this, items) - if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) { - // Close the database - sendBroadcast(Intent(LOCK_ACTION)) - } - finish() - }, - { - // Show the database UI to select the entry - GroupActivity.launchForAutofillResult(this, - assistStructure, - false, - searchInfo) - }, - { - // If database not open - FileDatabaseSelectActivity.launchForAutofillResult(this, - assistStructure, - searchInfo) - } - ) + launchSelection(searchInfo) } } SpecialMode.REGISTRATION -> { // To register info val registerInfo = intent.getParcelableExtra(KEY_REGISTER_INFO) val searchInfo = SearchInfo(registerInfo?.searchInfo) - if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, - PreferencesUtil.applicationIdBlocklist(this)) - || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, - PreferencesUtil.webDomainBlocklist(this))) { - // If item not allowed, show a toast - Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show() - setResult(Activity.RESULT_CANCELED) - } else { - SearchHelper.checkAutoSearchInfo(this, - Database.getInstance(), - searchInfo, - { _ -> - // Show the database UI to select the entry - GroupActivity.launchForRegistration(this, - registerInfo) - }, - { - // Show the database UI to select the entry - GroupActivity.launchForRegistration(this, - registerInfo) - }, - { - // If database not open - FileDatabaseSelectActivity.launchForRegistration(this, - registerInfo) - } - ) + if (!PreferencesUtil.searchSubdomains(this)) { + UriUtil.getWebDomainWithoutSubDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain -> + searchInfo.webDomain = webDomainWithoutSubDomain + launchRegistration(searchInfo, registerInfo) + } } - finish() + launchRegistration(searchInfo, registerInfo) } } } @@ -145,6 +89,84 @@ class AutofillLauncherActivity : AppCompatActivity() { super.onCreate(savedInstanceState) } + private fun launchSelection(searchInfo: SearchInfo) { + // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) + val assistStructure = AutofillHelper.retrieveAssistStructure(intent) + + if (assistStructure == null) { + setResult(Activity.RESULT_CANCELED) + finish() + } else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, + PreferencesUtil.applicationIdBlocklist(this)) + || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, + PreferencesUtil.webDomainBlocklist(this))) { + // If item not allowed, show a toast + Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show() + setResult(Activity.RESULT_CANCELED) + finish() + } else { + // If database is open + SearchHelper.checkAutoSearchInfo(this, + Database.getInstance(), + searchInfo, + { items -> + // Items found + AutofillHelper.buildResponse(this, items) + if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) { + // Close the database + sendBroadcast(Intent(LOCK_ACTION)) + } + finish() + }, + { + // Show the database UI to select the entry + GroupActivity.launchForAutofillResult(this, + assistStructure, + false, + searchInfo) + }, + { + // If database not open + FileDatabaseSelectActivity.launchForAutofillResult(this, + assistStructure, + searchInfo) + } + ) + } + } + + private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) { + if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, + PreferencesUtil.applicationIdBlocklist(this)) + || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, + PreferencesUtil.webDomainBlocklist(this))) { + // If item not allowed, show a toast + Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show() + setResult(Activity.RESULT_CANCELED) + } else { + SearchHelper.checkAutoSearchInfo(this, + Database.getInstance(), + searchInfo, + { _ -> + // Show the database UI to select the entry + GroupActivity.launchForRegistration(this, + registerInfo) + }, + { + // Show the database UI to select the entry + GroupActivity.launchForRegistration(this, + registerInfo) + }, + { + // If database not open + FileDatabaseSelectActivity.launchForRegistration(this, + registerInfo) + } + ) + } + finish() + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt index e8bf16900..5b7faec1a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt @@ -31,6 +31,7 @@ import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.utils.UriUtil /** * Activity to search or select entry in database, @@ -54,13 +55,25 @@ class EntrySelectionLauncherActivity : AppCompatActivity() { else -> {} } - // Setting to integrate Magikeyboard - val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this) - // Build search param val searchInfo = SearchInfo().apply { webDomain = sharedWebDomain } + if (!PreferencesUtil.searchSubdomains(this)) { + UriUtil.getWebDomainWithoutSubDomain(this, sharedWebDomain) { webDomainWithoutSubDomain -> + searchInfo.webDomain = webDomainWithoutSubDomain + launch(searchInfo) + } + } else { + launch(searchInfo) + } + + super.onCreate(savedInstanceState) + } + + private fun launch(searchInfo: SearchInfo) { + // Setting to integrate Magikeyboard + val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this) // If database is open SearchHelper.checkAutoSearchInfo(this, @@ -112,8 +125,6 @@ class EntrySelectionLauncherActivity : AppCompatActivity() { ) finish() - - super.onCreate(savedInstanceState) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index 28b60f430..988a96639 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -65,7 +65,6 @@ import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.model.getSearchString import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK @@ -364,7 +363,7 @@ class GroupActivity : LockingActivity(), val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false) if (searchInfo != null && autoSearch) { intent.action = Intent.ACTION_SEARCH - intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this)) + intent.putExtra(SearchManager.QUERY, searchInfo.toString()) return true } return false diff --git a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt index 1d89924f7..c0a9d56d8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/search/SearchHelper.kt @@ -28,7 +28,6 @@ import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorK import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.model.getSearchString import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.TimeoutHelper @@ -53,7 +52,7 @@ class SearchHelper { && !searchInfo.containsOnlyNullValues()) { // If search provide results database.createVirtualGroupFromSearchInfo( - searchInfo.getSearchString(context), + searchInfo.toString(), PreferencesUtil.omitBackup(context), MAX_SEARCH_ENTRY )?.let { searchGroup -> diff --git a/app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt index 1f8e0beb1..6838959c9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/SearchInfo.kt @@ -1,12 +1,9 @@ package com.kunzisoft.keepass.model -import android.content.Context import android.content.res.Resources import android.os.Parcel import android.os.Parcelable -import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.ObjectNameResource -import com.kunzisoft.keepass.utils.UriUtil class SearchInfo : ObjectNameResource, Parcelable { @@ -106,15 +103,4 @@ class SearchInfo : ObjectNameResource, Parcelable { } } } -} - -fun SearchInfo.getSearchString(context: Context): String { - return run { - if (!PreferencesUtil.searchSubdomains(context)) - UriUtil.getWebDomainWithoutSubDomain(webDomain) - else - webDomain - } - ?: applicationId - ?: "" } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt index e72afb6d4..3341e7f82 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -27,6 +27,10 @@ import android.os.Build import android.widget.Toast import androidx.documentfile.provider.DocumentFile import com.kunzisoft.keepass.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.lib.publicsuffixlist.PublicSuffixList import java.io.* import java.util.* @@ -94,17 +98,18 @@ object UriUtil { null } - fun getWebDomainWithoutSubDomain(webDomain: String?): String? { - webDomain?.split(".")?.let { domainArray -> - if (domainArray.isEmpty()) { - return "" + fun getWebDomainWithoutSubDomain(context: Context, + webDomain: String?, + webDomainWithoutSubDomain: (String?) -> Unit) { + CoroutineScope(Dispatchers.Main).launch { + if (webDomain != null) { + val publicSuffixList = PublicSuffixList(context) + webDomainWithoutSubDomain.invoke(publicSuffixList + .getPublicSuffixPlusOne(webDomain).await()) + } else { + webDomainWithoutSubDomain.invoke(null) } - if (domainArray.size == 1) { - return domainArray[0]; - } - return domainArray[domainArray.size - 2] + "." + domainArray[domainArray.size - 1] } - return null } fun decode(uri: String?): String { diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt b/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt new file mode 100644 index 000000000..c40ae0a3c --- /dev/null +++ b/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.lib.publicsuffixlist + +import android.content.Context +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async + +/** + * API for reading and accessing the public suffix list. + * + * > A "public suffix" is one under which Internet users can (or historically could) directly register names. Some + * > examples of public suffixes are .com, .co.uk and pvt.k12.ma.us. The Public Suffix List is a list of all known + * > public suffixes. + * + * Note that this implementation applies the rules of the public suffix list only and does not validate domains. + * + * https://publicsuffix.org/ + * https://github.com/publicsuffix/list + */ +class PublicSuffixList( + context: Context, + dispatcher: CoroutineDispatcher = Dispatchers.IO, + private val scope: CoroutineScope = CoroutineScope(dispatcher) +) { + private val data: PublicSuffixListData by lazy { PublicSuffixListLoader.load(context) } + + /** + * Prefetch the public suffix list from disk so that it is available in memory. + */ + fun prefetch(): Deferred = scope.async { + data.run { Unit } + } + + /** + * Returns true if the given [domain] is a public suffix; false otherwise. + * + * E.g.: + * ``` + * co.uk -> true + * com -> true + * mozilla.org -> false + * org -> true + * ``` + * + * Note that this method ignores the default "prevailing rule" described in the formal public suffix list algorithm: + * If no rule matches then the passed [domain] is assumed to *not* be a public suffix. + * + * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values + * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result. + */ + fun isPublicSuffix(domain: String): Deferred = scope.async { + when (data.getPublicSuffixOffset(domain)) { + is PublicSuffixOffset.PublicSuffix -> true + else -> false + } + } + + /** + * Returns the public suffix and one more level; known as the registrable domain. Returns `null` if + * [domain] is a public suffix itself. + * + * E.g.: + * ``` + * wwww.mozilla.org -> mozilla.org + * www.bcc.co.uk -> bbc.co.uk + * a.b.ide.kyoto.jp -> b.ide.kyoto.jp + * ``` + * + * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values + * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result. + */ + fun getPublicSuffixPlusOne(domain: String): Deferred = scope.async { + when (val offset = data.getPublicSuffixOffset(domain)) { + is PublicSuffixOffset.Offset -> domain + .split('.') + .drop(offset.value) + .joinToString(separator = ".") + else -> null + } + } + + /** + * Returns the public suffix of the given [domain]; known as the effective top-level domain (eTLD). Returns `null` + * if the [domain] is a public suffix itself. + * + * E.g.: + * ``` + * wwww.mozilla.org -> org + * www.bcc.co.uk -> co.uk + * a.b.ide.kyoto.jp -> ide.kyoto.jp + * ``` + * + * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values + * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result. + */ + fun getPublicSuffix(domain: String) = scope.async { + when (val offset = data.getPublicSuffixOffset(domain)) { + is PublicSuffixOffset.Offset -> domain + .split('.') + .drop(offset.value + 1) + .joinToString(separator = ".") + else -> null + } + } + + /** + * Strips the public suffix from the given [domain]. Returns the original domain if no public suffix could be + * stripped. + * + * E.g.: + * ``` + * wwww.mozilla.org -> www.mozilla + * www.bcc.co.uk -> www.bbc + * a.b.ide.kyoto.jp -> a.b + * ``` + * + * @param [domain] _must_ be a valid domain. [PublicSuffixList] performs no validation, and if any unexpected values + * are passed (e.g., a full URL, a domain with a trailing '/', etc) this may return an incorrect result. + */ + fun stripPublicSuffix(domain: String) = scope.async { + when (val offset = data.getPublicSuffixOffset(domain)) { + is PublicSuffixOffset.Offset -> domain + .split('.') + .joinToString(separator = ".", limit = offset.value + 1, truncated = "") + .dropLast(1) + else -> domain + } + } +} diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt b/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt new file mode 100644 index 000000000..0439b304d --- /dev/null +++ b/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt @@ -0,0 +1,158 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.lib.publicsuffixlist + +import mozilla.components.lib.publicsuffixlist.ext.binarySearch +import java.net.IDN + +/** + * Class wrapping the public suffix list data and offering methods for accessing rules in it. + */ +internal class PublicSuffixListData( + private val rules: ByteArray, + private val exceptions: ByteArray +) { + private fun binarySearchRules(labels: List, labelIndex: Int): String? { + return rules.binarySearch(labels, labelIndex) + } + + private fun binarySearchExceptions(labels: List, labelIndex: Int): String? { + return exceptions.binarySearch(labels, labelIndex) + } + + @Suppress("ReturnCount") + fun getPublicSuffixOffset(domain: String): PublicSuffixOffset? { + if (domain.isEmpty()) { + return null + } + + val domainLabels = IDN.toUnicode(domain).split('.') + if (domainLabels.find { it.isEmpty() } != null) { + // At least one of the labels is empty: Bail out. + return null + } + + val rule = findMatchingRule(domainLabels) + + if (domainLabels.size == rule.size && rule[0][0] != PublicSuffixListData.EXCEPTION_MARKER) { + // The domain is a public suffix. + return if (rule == PublicSuffixListData.PREVAILING_RULE) { + PublicSuffixOffset.PrevailingRule + } else { + PublicSuffixOffset.PublicSuffix + } + } + + return if (rule[0][0] == PublicSuffixListData.EXCEPTION_MARKER) { + // Exception rules hold the effective TLD plus one. + PublicSuffixOffset.Offset(domainLabels.size - rule.size) + } else { + // Otherwise the rule is for a public suffix, so we must take one more label. + PublicSuffixOffset.Offset(domainLabels.size - (rule.size + 1)) + } + } + + /** + * Find a matching rule for the given domain labels. + * + * This algorithm is based on OkHttp's PublicSuffixDatabase class: + * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java + */ + private fun findMatchingRule(domainLabels: List): List { + // Break apart the domain into UTF-8 labels, i.e. foo.bar.com turns into [foo, bar, com]. + val domainLabelsBytes = domainLabels.map { it.toByteArray(Charsets.UTF_8) } + + val exactMatch = findExactMatch(domainLabelsBytes) + val wildcardMatch = findWildcardMatch(domainLabelsBytes) + val exceptionMatch = findExceptionMatch(domainLabelsBytes, wildcardMatch) + + if (exceptionMatch != null) { + return ("${PublicSuffixListData.EXCEPTION_MARKER}$exceptionMatch").split('.') + } + + if (exactMatch == null && wildcardMatch == null) { + return PublicSuffixListData.PREVAILING_RULE + } + + val exactRuleLabels = exactMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE + val wildcardRuleLabels = wildcardMatch?.split('.') ?: PublicSuffixListData.EMPTY_RULE + + return if (exactRuleLabels.size > wildcardRuleLabels.size) { + exactRuleLabels + } else { + wildcardRuleLabels + } + } + + /** + * Returns an exact match or null. + */ + private fun findExactMatch(labels: List): String? { + // Start by looking for exact matches. We start at the leftmost label. For example, foo.bar.com + // will look like: [foo, bar, com], [bar, com], [com]. The longest matching rule wins. + + for (i in 0 until labels.size) { + val rule = binarySearchRules(labels, i) + + if (rule != null) { + return rule + } + } + + return null + } + + /** + * Returns a wildcard match or null. + */ + private fun findWildcardMatch(labels: List): String? { + // In theory, wildcard rules are not restricted to having the wildcard in the leftmost position. + // In practice, wildcards are always in the leftmost position. For now, this implementation + // cheats and does not attempt every possible permutation. Instead, it only considers wildcards + // in the leftmost position. We assert this fact when we generate the public suffix file. If + // this assertion ever fails we'll need to refactor this implementation. + if (labels.size > 1) { + val labelsWithWildcard = labels.toMutableList() + for (labelIndex in 0 until labelsWithWildcard.size) { + labelsWithWildcard[labelIndex] = PublicSuffixListData.WILDCARD_LABEL + val rule = binarySearchRules(labelsWithWildcard, labelIndex) + if (rule != null) { + return rule + } + } + } + + return null + } + + private fun findExceptionMatch(labels: List, wildcardMatch: String?): String? { + // Exception rules only apply to wildcard rules, so only try it if we matched a wildcard. + if (wildcardMatch == null) { + return null + } + + for (labelIndex in 0 until labels.size) { + val rule = binarySearchExceptions(labels, labelIndex) + if (rule != null) { + return rule + } + } + + return null + } + + companion object { + val WILDCARD_LABEL = byteArrayOf('*'.toByte()) + val PREVAILING_RULE = listOf("*") + val EMPTY_RULE = listOf() + const val EXCEPTION_MARKER = '!' + } +} + +internal sealed class PublicSuffixOffset { + data class Offset(val value: Int) : PublicSuffixOffset() + object PublicSuffix : PublicSuffixOffset() + object PrevailingRule : PublicSuffixOffset() +} diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt b/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt new file mode 100644 index 000000000..59a671fde --- /dev/null +++ b/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.lib.publicsuffixlist + +import android.content.Context +import java.io.BufferedInputStream +import java.io.IOException + +private const val PUBLIC_SUFFIX_LIST_FILE = "publicsuffixes" + +internal object PublicSuffixListLoader { + fun load(context: Context): PublicSuffixListData = context.assets.open( + PUBLIC_SUFFIX_LIST_FILE + ).buffered().use { stream -> + val publicSuffixSize = stream.readInt() + val publicSuffixBytes = stream.readFully(publicSuffixSize) + + val exceptionSize = stream.readInt() + val exceptionBytes = stream.readFully(exceptionSize) + + PublicSuffixListData(publicSuffixBytes, exceptionBytes) + } +} + +@Suppress("MagicNumber") +private fun BufferedInputStream.readInt(): Int { + return (read() and 0xff shl 24 + or (read() and 0xff shl 16) + or (read() and 0xff shl 8) + or (read() and 0xff)) +} + +private fun BufferedInputStream.readFully(size: Int): ByteArray { + val bytes = ByteArray(size) + + var offset = 0 + while (offset < size) { + val read = read(bytes, offset, size - offset) + if (read == -1) { + throw IOException("Unexpected end of stream") + } + offset += read + } + + return bytes +} diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt b/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt new file mode 100644 index 000000000..7316e4053 --- /dev/null +++ b/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.lib.publicsuffixlist.ext + +import kotlin.experimental.and + +private const val BITMASK = 0xff.toByte() + +/** + * Performs a binary search for the provided [labels] on the [ByteArray]'s data. + * + * This algorithm is based on OkHttp's PublicSuffixDatabase class: + * https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/publicsuffix/PublicSuffixDatabase.java + */ +@Suppress("ComplexMethod", "NestedBlockDepth") +internal fun ByteArray.binarySearch(labels: List, labelIndex: Int): String? { + var low = 0 + var high = size + var match: String? = null + + while (low < high) { + val mid = (low + high) / 2 + val start = findStartOfLineFromIndex(mid) + val end = findEndOfLineFromIndex(start) + + val publicSuffixLength = start + end - start + + var compareResult: Int + var currentLabelIndex = labelIndex + var currentLabelByteIndex = 0 + var publicSuffixByteIndex = 0 + + var expectDot = false + while (true) { + val byte0 = if (expectDot) { + expectDot = false + '.'.toByte() + } else { + labels[currentLabelIndex][currentLabelByteIndex] and BITMASK + } + + val byte1 = this[start + publicSuffixByteIndex] and BITMASK + + // Compare the bytes. Note that the file stores UTF-8 encoded bytes, so we must compare the + // unsigned bytes. + @Suppress("EXPERIMENTAL_API_USAGE") + compareResult = (byte0.toUByte() - byte1.toUByte()).toInt() + if (compareResult != 0) { + break + } + + publicSuffixByteIndex++ + currentLabelByteIndex++ + + if (publicSuffixByteIndex == publicSuffixLength) { + break + } + + if (labels[currentLabelIndex].size == currentLabelByteIndex) { + // We've exhausted our current label. Either there are more labels to compare, in which + // case we expect a dot as the next character. Otherwise, we've checked all our labels. + if (currentLabelIndex == labels.size - 1) { + break + } else { + currentLabelIndex++ + currentLabelByteIndex = -1 + expectDot = true + } + } + } + + if (compareResult < 0) { + high = start - 1 + } else if (compareResult > 0) { + low = start + end + 1 + } else { + // We found a match, but are the lengths equal? + val publicSuffixBytesLeft = publicSuffixLength - publicSuffixByteIndex + var labelBytesLeft = labels[currentLabelIndex].size - currentLabelByteIndex + for (i in currentLabelIndex + 1 until labels.size) { + labelBytesLeft += labels[i].size + } + + if (labelBytesLeft < publicSuffixBytesLeft) { + high = start - 1 + } else if (labelBytesLeft > publicSuffixBytesLeft) { + low = start + end + 1 + } else { + // Found a match. + match = String(this, start, publicSuffixLength, Charsets.UTF_8) + break + } + } + } + + return match +} + +/** + * Search for a '\n' that marks the start of a value. Don't go back past the start of the array. + */ +private fun ByteArray.findStartOfLineFromIndex(start: Int): Int { + var index = start + while (index > -1 && this[index] != '\n'.toByte()) { + index-- + } + index++ + return index +} + +/** + * Search for a '\n' that marks the end of a value. + */ +private fun ByteArray.findEndOfLineFromIndex(start: Int): Int { + var end = 1 + while (this[start + end] != '\n'.toByte()) { + end++ + } + return end +}