mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Detect web requests from browsers other than Chrome
IntentUtils only treated a nostrsigner: request as coming from a web page when Chrome's Browser.EXTRA_APPLICATION_ID extra was present. Browsers like DuckDuckGo don't set it, so their URL-encoded requests were parsed as intent extras and failed with "Unknown signer type". The request is now parsed from the URI when Chrome's extra is present, or, if there is no native `type` extra, when the intent has the BROWSABLE category, the referrer is a browser (http(s) referrer or a package that handles https links), or the URI carries a `type=` query parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c3eb08974e
commit
54f9e2eebd
@@ -6,6 +6,12 @@
|
||||
<package android:name="com.greenart7c3.nostrsigner.debug" />
|
||||
<package android:name="com.greenart7c3.nostrsigner" />
|
||||
<package android:name="dev.zapstore.app" />
|
||||
<!-- Lets IntentUtils tell whether a nostrsigner: request was launched by a browser -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@@ -55,7 +55,7 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
Amber.instance.setMainActivity(this)
|
||||
mainViewModel = MainViewModel(applicationContext)
|
||||
intent?.let { mainViewModel.onNewIntent(it, callingPackage) }
|
||||
intent?.let { mainViewModel.onNewIntent(it, callingPackage, referrer) }
|
||||
setContent {
|
||||
HttpClientManager.setDefaultUserAgent("Amber/${BuildConfig.VERSION_NAME}")
|
||||
|
||||
@@ -141,6 +141,7 @@ class MainActivity : AppCompatActivity() {
|
||||
intent = remember(intent) { IntentWrapper(intent) },
|
||||
packageName = packageName,
|
||||
appName = appName,
|
||||
referrer = referrer,
|
||||
bunkerRequests = bunkerRequests.value,
|
||||
navController = NavHostControllerWrapper(navController),
|
||||
)
|
||||
@@ -176,7 +177,7 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
mainViewModel.onNewIntent(intent, callingPackage)
|
||||
mainViewModel.onNewIntent(intent, callingPackage, referrer)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.greenart7c3.nostrsigner
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -126,11 +127,12 @@ class MainViewModel(val context: Context) : ViewModel() {
|
||||
fun onNewIntent(
|
||||
intent: Intent,
|
||||
callingPackage: String?,
|
||||
referrer: Uri? = null,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val account = LocalPreferences.loadFromEncryptedStorage(context)
|
||||
account?.let { acc ->
|
||||
val intentData = IntentUtils.getIntentData(context, intent, callingPackage, intent.getStringExtra("route"), acc)
|
||||
val intentData = IntentUtils.getIntentData(context, intent, callingPackage, intent.getStringExtra("route"), acc, referrer)
|
||||
if (intentData != null) {
|
||||
IntentUtils.addAll(listOf(intentData))
|
||||
addedIntentIds.add(intentData.id)
|
||||
|
||||
@@ -68,7 +68,7 @@ class SignerActivity : AppCompatActivity() {
|
||||
|
||||
Amber.instance.setMainActivity(this)
|
||||
mainViewModel = MainViewModel(applicationContext)
|
||||
intent?.let { mainViewModel.onNewIntent(it, callingPackage) }
|
||||
intent?.let { mainViewModel.onNewIntent(it, callingPackage, referrer) }
|
||||
setContent {
|
||||
NostrSignerTheme {
|
||||
HttpClientManager.setDefaultUserAgent("Amber/${BuildConfig.VERSION_NAME}")
|
||||
@@ -180,6 +180,7 @@ class SignerActivity : AppCompatActivity() {
|
||||
intent = remember(intent) { IntentWrapper(intent) },
|
||||
packageName = packageName,
|
||||
appName = appName,
|
||||
referrer = referrer,
|
||||
bunkerRequests = bunkerRequests.value,
|
||||
navController = NavHostControllerWrapper(navController),
|
||||
isExternalRequest = true,
|
||||
@@ -221,7 +222,7 @@ class SignerActivity : AppCompatActivity() {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
if (::mainViewModel.isInitialized) {
|
||||
mainViewModel.onNewIntent(intent, callingPackage)
|
||||
mainViewModel.onNewIntent(intent, callingPackage, referrer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity.RESULT_OK
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.Browser
|
||||
import android.widget.Toast
|
||||
@@ -709,12 +710,70 @@ object IntentUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the launching app is a web browser. [referrer] is the value of
|
||||
* `Activity.getReferrer()`: an `http(s)` referrer is the web page itself, an
|
||||
* `android-app://<package>` referrer is the launching app.
|
||||
*/
|
||||
fun isBrowserReferrer(context: Context, referrer: Uri?): Boolean = when (referrer?.scheme) {
|
||||
"http", "https" -> true
|
||||
"android-app" -> isBrowserPackage(context, referrer.host)
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun isBrowserPackage(context: Context, packageName: String?): Boolean {
|
||||
if (packageName.isNullOrBlank()) return false
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, "https://example.com".toUri())
|
||||
.addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
.setPackage(packageName)
|
||||
return try {
|
||||
context.packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_ALL).isNotEmpty()
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides whether a `nostrsigner:` request carries its parameters in the URI query
|
||||
* (web apps) instead of intent extras (native NIP-55 apps). Browsers don't agree on
|
||||
* how they mark external intents: Chrome adds [Browser.EXTRA_APPLICATION_ID], others
|
||||
* (e.g. DuckDuckGo) only add [Intent.CATEGORY_BROWSABLE] or nothing at all. A `type`
|
||||
* extra means the caller used the native extras API, so only then do we ignore the
|
||||
* weaker signals (BROWSABLE category, browser referrer, `type=` query parameter).
|
||||
*/
|
||||
internal fun isWebRequest(
|
||||
hasApplicationIdExtra: Boolean,
|
||||
hasBrowsableCategory: Boolean,
|
||||
referrerIsBrowser: Boolean,
|
||||
typeExtra: String?,
|
||||
dataString: String?,
|
||||
): Boolean {
|
||||
if (hasApplicationIdExtra) return true
|
||||
if (!typeExtra.isNullOrBlank()) return false
|
||||
return hasBrowsableCategory || referrerIsBrowser || hasTypeQueryParameter(dataString)
|
||||
}
|
||||
|
||||
private fun hasTypeQueryParameter(dataString: String?): Boolean {
|
||||
if (dataString.isNullOrBlank()) return false
|
||||
val decoded = try {
|
||||
URLDecoder.decode(dataString.replace("+", "%2b"), "utf-8")
|
||||
} catch (_: Exception) {
|
||||
dataString
|
||||
}
|
||||
return listOf(dataString, decoded).any { data ->
|
||||
data.substringAfter('?', "")
|
||||
.split('?', '&')
|
||||
.any { it.startsWith("type=") }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getIntentData(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
packageName: String?,
|
||||
route: String?,
|
||||
currentLoggedInAccount: Account,
|
||||
referrer: Uri? = null,
|
||||
): IntentData? {
|
||||
try {
|
||||
if (intent.data == null) {
|
||||
@@ -748,10 +807,18 @@ object IntentUtils {
|
||||
|
||||
if (intent.dataString?.startsWith("nostrconnect:") == true) {
|
||||
NostrConnectUtils.getIntentFromNostrConnect(intent, localAccount)
|
||||
} else if (intent.extras?.getString(Browser.EXTRA_APPLICATION_ID) == null) {
|
||||
return getIntentDataFromIntent(context, intent, packageName, route, localAccount)
|
||||
} else {
|
||||
} else if (
|
||||
isWebRequest(
|
||||
hasApplicationIdExtra = intent.extras?.getString(Browser.EXTRA_APPLICATION_ID) != null,
|
||||
hasBrowsableCategory = intent.hasCategory(Intent.CATEGORY_BROWSABLE),
|
||||
referrerIsBrowser = isBrowserReferrer(context, referrer),
|
||||
typeExtra = intent.extras?.getString("type"),
|
||||
dataString = intent.dataString,
|
||||
)
|
||||
) {
|
||||
return getIntentDataWithoutExtras(context, intent.data?.toString() ?: "", intent, packageName, route, localAccount)
|
||||
} else {
|
||||
return getIntentDataFromIntent(context, intent, packageName, route, localAccount)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AmberLog.e(Amber.TAG, "Error parsing intent: ${e.message}", e)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -54,6 +55,7 @@ fun AccountScreen(
|
||||
bunkerRequests: ImmutableList<AmberBunkerRequest>,
|
||||
navController: NavHostControllerWrapper,
|
||||
isExternalRequest: Boolean = false,
|
||||
referrer: Uri? = null,
|
||||
) {
|
||||
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||
val context = LocalContext.current
|
||||
@@ -89,6 +91,7 @@ fun AccountScreen(
|
||||
packageName,
|
||||
it.getStringExtra("route"),
|
||||
state.account,
|
||||
referrer,
|
||||
)?.let { intentData ->
|
||||
IntentUtils.addAll(listOf(intentData))
|
||||
}
|
||||
|
||||
@@ -192,6 +192,53 @@ class IntentUtilsTest {
|
||||
assertNull(IntentUtils.isRemembered(signPolicy = 1, permission = null))
|
||||
}
|
||||
|
||||
private fun isWebRequest(
|
||||
hasApplicationIdExtra: Boolean = false,
|
||||
hasBrowsableCategory: Boolean = false,
|
||||
referrerIsBrowser: Boolean = false,
|
||||
typeExtra: String? = null,
|
||||
dataString: String? = "nostrsigner:",
|
||||
) = IntentUtils.isWebRequest(hasApplicationIdExtra, hasBrowsableCategory, referrerIsBrowser, typeExtra, dataString)
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is true for Chrome application id extra`() {
|
||||
assertTrue(isWebRequest(hasApplicationIdExtra = true, typeExtra = "sign_event"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is true for browsable category`() {
|
||||
assertTrue(isWebRequest(hasBrowsableCategory = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is true when referrer is a browser`() {
|
||||
assertTrue(isWebRequest(referrerIsBrowser = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is true for type query parameter without browser markers`() {
|
||||
assertTrue(isWebRequest(dataString = "nostrsigner:?type=get_public_key&callbackUrl=https://example.com/?event="))
|
||||
assertTrue(isWebRequest(dataString = "nostrsigner:%7B%22kind%22%3A1%7D?compressionType=none&type=sign_event"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is true for url encoded type query parameter`() {
|
||||
assertTrue(isWebRequest(dataString = "nostrsigner:abc%3Ftype%3Dnip04_encrypt%26pubkey%3D123"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is false for native app with type extra`() {
|
||||
assertFalse(isWebRequest(typeExtra = "sign_event", dataString = "nostrsigner:{\"content\":\"what?type=x\"}"))
|
||||
assertFalse(isWebRequest(hasBrowsableCategory = true, referrerIsBrowser = true, typeExtra = "sign_event"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isWebRequest is false without markers or query`() {
|
||||
assertFalse(isWebRequest())
|
||||
assertFalse(isWebRequest(dataString = null))
|
||||
assertFalse(isWebRequest(dataString = "nostrsigner:?pubkey=123"))
|
||||
}
|
||||
|
||||
// Helper to build a minimal ApplicationPermissionsEntity for isRemembered tests
|
||||
private fun permissionWith(
|
||||
acceptable: Boolean = true,
|
||||
|
||||
Reference in New Issue
Block a user