mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Fix subdomain search #728
This commit is contained in:
BIN
app/src/main/assets/publicsuffixes
Normal file
BIN
app/src/main/assets/publicsuffixes
Normal file
Binary file not shown.
@@ -40,6 +40,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
@@ -61,83 +62,26 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||||
}
|
}
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
if (!PreferencesUtil.searchSubdomains(this)) {
|
||||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
UriUtil.getWebDomainWithoutSubDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
if (assistStructure == null) {
|
launchSelection(searchInfo)
|
||||||
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 {
|
} else {
|
||||||
// If database is open
|
launchSelection(searchInfo)
|
||||||
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)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
if (!PreferencesUtil.searchSubdomains(this)) {
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
UriUtil.getWebDomainWithoutSubDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
PreferencesUtil.webDomainBlocklist(this))) {
|
launchRegistration(searchInfo, registerInfo)
|
||||||
// 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()
|
launchRegistration(searchInfo, registerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +89,84 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
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?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import com.kunzisoft.keepass.magikeyboard.MagikIME
|
|||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -54,13 +55,25 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setting to integrate Magikeyboard
|
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
|
||||||
|
|
||||||
// Build search param
|
// Build search param
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
webDomain = sharedWebDomain
|
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
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
@@ -112,8 +125,6 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ import com.kunzisoft.keepass.education.GroupActivityEducation
|
|||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
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_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_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
|
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)
|
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
|
||||||
if (searchInfo != null && autoSearch) {
|
if (searchInfo != null && autoSearch) {
|
||||||
intent.action = Intent.ACTION_SEARCH
|
intent.action = Intent.ACTION_SEARCH
|
||||||
intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this))
|
intent.putExtra(SearchManager.QUERY, searchInfo.toString())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorK
|
|||||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.model.getSearchString
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ class SearchHelper {
|
|||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo.getSearchString(context),
|
searchInfo.toString(),
|
||||||
PreferencesUtil.omitBackup(context),
|
PreferencesUtil.omitBackup(context),
|
||||||
MAX_SEARCH_ENTRY
|
MAX_SEARCH_ENTRY
|
||||||
)?.let { searchGroup ->
|
)?.let { searchGroup ->
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class SearchInfo : ObjectNameResource, Parcelable {
|
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
|
|
||||||
?: ""
|
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,10 @@ import android.os.Build
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.kunzisoft.keepass.R
|
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.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -94,17 +98,18 @@ object UriUtil {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getWebDomainWithoutSubDomain(webDomain: String?): String? {
|
fun getWebDomainWithoutSubDomain(context: Context,
|
||||||
webDomain?.split(".")?.let { domainArray ->
|
webDomain: String?,
|
||||||
if (domainArray.isEmpty()) {
|
webDomainWithoutSubDomain: (String?) -> Unit) {
|
||||||
return ""
|
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 {
|
fun decode(uri: String?): String {
|
||||||
|
|||||||
@@ -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<Unit> = 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<Boolean> = 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<String?> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ByteArray>, labelIndex: Int): String? {
|
||||||
|
return rules.binarySearch(labels, labelIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun binarySearchExceptions(labels: List<ByteArray>, 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<String>): List<String> {
|
||||||
|
// 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<ByteArray>): 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<ByteArray>): 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<ByteArray>, 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<String>()
|
||||||
|
const val EXCEPTION_MARKER = '!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PublicSuffixOffset {
|
||||||
|
data class Offset(val value: Int) : PublicSuffixOffset()
|
||||||
|
object PublicSuffix : PublicSuffixOffset()
|
||||||
|
object PrevailingRule : PublicSuffixOffset()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<ByteArray>, 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user