Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ internal data class AppData(
val label: String,
val lastUpdateTime: Long,
val appPath: String,
val iconPath: String = ""
val iconPath: String = "",
val isBrowser: Boolean = false
) {
fun hasCachedIcon(ctx: Context, sizePx: Int, dpi: Int): Boolean =
fun hasCachedIcon(ctx: Context, sizePx: Int, dpi: Int): Boolean =
IconCache.pathFor(ctx, packageName, lastUpdateTime, sizePx, dpi).exists()

fun withIconPathIfCached(ctx: Context, sizePx: Int, dpi: Int): AppData {
Expand All @@ -23,6 +24,7 @@ internal data class AppData(
"label" to label,
"name" to label,
"appPath" to appPath,
"iconPath" to iconPath
"iconPath" to iconPath,
"isBrowser" to isBrowser
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.*
import android.net.Uri
import android.os.Build
import androidx.core.content.ContextCompat
import io.flutter.plugin.common.EventChannel
Expand Down Expand Up @@ -110,13 +111,14 @@ internal class AppDataHandler(
Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
PackageManager.MATCH_ALL
)
val browsers = browserPackages(pm)

val entries = launchables.mapNotNull { ri ->
val pkg = ri.activityInfo?.packageName ?: return@mapNotNull null
if (AppFilters.shouldSkip(pkg, lanternPkg)) return@mapNotNull null
val label = runCatching { ri.loadLabel(pm).toString() }.getOrDefault(pkg)
val lastUpdate = runCatching { pm.getPackageInfoCompat(pkg).lastUpdateTime }.getOrDefault(0L)
AppData(pkg, label, lastUpdate, appPath = "")
AppData(pkg, label, lastUpdate, appPath = "", isBrowser = pkg in browsers)
}
.distinctBy { it.packageName }
.sortedBy { it.label.lowercase(Locale.getDefault()) }
Expand Down Expand Up @@ -227,9 +229,26 @@ internal class AppDataHandler(
pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString()
}.getOrDefault(pkg)
val lastUpdate = runCatching { pm.getPackageInfoCompat(pkg).lastUpdateTime }.getOrDefault(0L)
return AppData(pkg, label, lastUpdate, appPath = "")
return AppData(pkg, label, lastUpdate, appPath = "", isBrowser = pkg in browserPackages(pm))
}

/**
* Packages that register themselves as http(s) handlers — i.e. browsers.
* Dynamic per device, so any installed browser is detected, not just a
* hardcoded list of the common ones.
*/
private fun browserPackages(pm: PackageManager): Set<String> = runCatching {
// Query both schemes: an app registering only https would otherwise
// slip through with isBrowser = false.
listOf("http", "https").flatMap { scheme ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$scheme://example.com"))
.addCategory(Intent.CATEGORY_BROWSABLE)
pm.queryIntentActivities(intent, PackageManager.MATCH_ALL)
}
.mapNotNull { it.activityInfo?.packageName }
.toSet()
Comment thread
jigar-f marked this conversation as resolved.
}.getOrDefault(emptySet())

private fun isSystemApp(pm: PackageManager, pkg: String): Boolean = runCatching {
val ai = pm.getApplicationInfo(pkg, 0)
(ai.flags and ApplicationInfo.FLAG_SYSTEM) != 0 ||
Expand Down
34 changes: 34 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -1727,3 +1727,37 @@ msgstr "Order Total"

msgid "smart_routing_mode_description"
msgstr "Smart Location picks the fastest server and switches automatically as it finds better routes."


msgid "bypass_browser_warning_title"
msgstr "Add %s to the bypass list?"

msgid "bypass_browser_warning_body"
msgstr "%s will connect directly, without Lantern's protection. In countries that block certain websites, some sites will stop loading."

msgid "add_anyway"
msgstr "Add Anyway"

msgid "bypass_all_warning_title"
msgstr "Add all apps to the bypass list?"

msgid "bypass_all_warning_body"
msgstr "This includes browsers like %s. Apps on the bypass list connect directly, without Lantern's protection. In countries that block certain websites, some sites will stop loading."

msgid "add_all_except_browsers"
msgstr "Add All Except Browsers"

msgid "add_all_anyway"
msgstr "Add All Anyway"

msgid "bypass_app_first_time_title"
msgstr "Bypass the VPN for this app?"

msgid "bypass_app_first_time_body"
msgstr "%s will connect directly, without Lantern's protection. If it's blocked in your country, it may stop working."

msgid "bypass_website_first_time_title"
msgstr "Bypass the VPN for this website?"

msgid "bypass_website_first_time_body"
msgstr "%s will connect directly, without Lantern's protection. If it's blocked in your country, it may stop working."
7 changes: 7 additions & 0 deletions lantern-core/apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ func LoadInstalledAppsWithDirs(dataDir string, appDirs []string, excludeDirs []s
var deferredWindowsCache []*AppData

if cached, err := loadCacheFromFile(dataDir); err == nil {
// Recompute rather than trust flags persisted by an older cache.
markBrowsers(cached)
for _, app := range cached {
if app == nil {
continue
Expand Down Expand Up @@ -273,6 +275,11 @@ func LoadInstalledAppsWithDirs(dataDir string, appDirs []string, excludeDirs []s
}

found := loadInstalledAppsPlatform(appDirs, seen, excludeDirs, cb)
// The callback has already streamed these pointers to the caller
// (LanternCore.LoadInstalledApps collects them and marshals after we
// return), so mutating them here is visible in the final JSON and in
// the cache saved below.
markBrowsers(found)
Comment thread
jigar-f marked this conversation as resolved.
if runtime.GOOS == "windows" {
for _, app := range deferredWindowsCache {
if app == nil {
Expand Down
10 changes: 5 additions & 5 deletions lantern-core/apps/apps_data.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package apps

type AppData struct {
Name string `json:"name"`
BundleID string `json:"bundleId"`
AppPath string `json:"appPath"`
IconPath string `json:"iconPath"`

Name string `json:"name"`
BundleID string `json:"bundleId"`
AppPath string `json:"appPath"`
IconPath string `json:"iconPath"`
IsBrowser bool `json:"isBrowser"`
IconBytes []byte `json:"iconBytes,omitempty"`
}
8 changes: 8 additions & 0 deletions lantern-core/apps/browsers_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !windows

package apps

// markBrowsers is a no-op outside Windows: Android and macOS detect
// browsers in their platform layers (AppDataHandler.kt queries browsable
// http intent handlers; AppStreamHandler.swift asks LaunchServices).
func markBrowsers([]*AppData) {}
199 changes: 199 additions & 0 deletions lantern-core/apps/browsers_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//go:build windows

package apps

import (
"log/slog"
"path/filepath"
"sort"
"strings"
"sync"

"golang.org/x/sys/windows/registry"
)

// browserExeIndex holds the executables of the browsers registered on this
// machine, keyed by normalized full path and by lowercase basename.
type browserExeIndex struct {
paths map[string]bool
basenames map[string]bool
}

// registryRoot pairs a registry hive with the access flags used to read it.
type registryRoot struct {
root registry.Key
flags uint32
}

// browserRegistryRoots covers the hives/views installed browsers register
// under: both WOW64 views of HKLM (machine-wide installs) plus HKCU
// (per-user installs, and packaged browsers whose default-app registration
// lives under the current user).
var browserRegistryRoots = []registryRoot{
{registry.LOCAL_MACHINE, registry.READ | registry.WOW64_64KEY},
{registry.LOCAL_MACHINE, registry.READ | registry.WOW64_32KEY},
{registry.CURRENT_USER, registry.READ | registry.WOW64_64KEY},
}

// loadBrowserIndexOnce builds the set of registered browser executables from
// the modern default-app registration, so any properly installed browser is
// detected without a hardcoded list:
//
// Software\RegisteredApplications -> Capabilities\URLAssociations
//
// Each RegisteredApplications value points at a Capabilities key, and an app
// is a browser exactly when its URLAssociations declare an http/https handler.
// This covers both classic Win32 browsers (Chrome, Firefox, Edge, ...) and
// packaged/MSIX browsers (e.g. Arc), which register here rather than under the
// legacy Software\Clients\StartMenuInternet key.
//
// Portable browsers that skip registration entirely (e.g. Tor Browser) are
// not caught.
var loadBrowserIndexOnce = sync.OnceValue(func() browserExeIndex {
idx := browserExeIndex{
paths: map[string]bool{},
basenames: map[string]bool{},
}

scanRegisteredApplicationBrowsers(&idx)

slog.Info("browser registry scan complete",
"browsers", len(idx.paths),
"basenames", sortedKeys(idx.basenames),
)
return idx
})

// addExe records a browser executable under both its full path and basename.
func (idx *browserExeIndex) addExe(exe string) {
if exe == "" {
return
}
idx.paths[normalizeKey(filepath.Clean(exe))] = true
idx.basenames[strings.ToLower(filepath.Base(exe))] = true
}

// scanRegisteredApplicationBrowsers indexes browsers via the modern
// default-app registration: each Software\RegisteredApplications value points
// at a Capabilities key, and an app is a browser exactly when its
// Capabilities\URLAssociations declares an http/https handler. This catches
// classic Win32 browsers as well as packaged/MSIX browsers (Arc,
// Store-installed Chromium forks, ...).
func scanRegisteredApplicationBrowsers(idx *browserExeIndex) {
const regAppsKey = `Software\RegisteredApplications`
for _, r := range browserRegistryRoots {
k, err := registry.OpenKey(r.root, regAppsKey, r.flags)
if err != nil {
continue
}
valueNames, _ := k.ReadValueNames(-1)
capPaths := make([]string, 0, len(valueNames))
for _, vn := range valueNames {
capPath, _, err := k.GetStringValue(vn)
if err == nil && strings.TrimSpace(capPath) != "" {
capPaths = append(capPaths, strings.TrimSpace(capPath))
}
}
k.Close()

for _, capPath := range capPaths {
addBrowserFromCapabilities(idx, r, capPath)
}
}
}

// addBrowserFromCapabilities inspects a Capabilities key and, when it
// declares an http/https URL handler, resolves that handler's executable into
// the index.
func addBrowserFromCapabilities(idx *browserExeIndex, r registryRoot, capPath string) {
ua, err := registry.OpenKey(r.root, capPath+`\URLAssociations`, r.flags)
if err != nil {
return
}
defer ua.Close()

for _, scheme := range []string{"http", "https"} {
progID, _, err := ua.GetStringValue(scheme)
if err != nil || strings.TrimSpace(progID) == "" {
continue
}
idx.addExe(browserExeFromProgID(r, strings.TrimSpace(progID)))
}
}

// browserExeFromProgID resolves a URL-handler ProgID to its executable via
// Software\Classes\<progID>\shell\open\command. It checks the ProgID's own
// hive first, then the opposite hive, since a per-user default may point at a
// machine-registered ProgID (or vice versa).
func browserExeFromProgID(r registryRoot, progID string) string {
roots := []registry.Key{r.root}
if r.root == registry.CURRENT_USER {
roots = append(roots, registry.LOCAL_MACHINE)
} else {
roots = append(roots, registry.CURRENT_USER)
}
for _, root := range roots {
ck, err := registry.OpenKey(
root, `Software\Classes\`+progID+`\shell\open\command`, r.flags)
if err != nil {
continue
}
cmd, _, _ := ck.GetStringValue("")
ck.Close()
if exe := browserCommandExe(cmd); exe != "" {
return exe
}
}
return ""
}

// browserCommandExe extracts the executable path from a shell\open\command
// value (typically a quoted exe path, sometimes with arguments).
func browserCommandExe(cmd string) string {
tokens := parseWindowsCommandTokens(cmd)
if len(tokens) == 0 {
return ""
}
exe := strings.Trim(strings.TrimSpace(tokens[0]), `"`)
if exe == "" {
return ""
}
exe = filepath.Clean(expandPercentEnv(exe))
if !filepath.IsAbs(exe) || !strings.EqualFold(filepath.Ext(exe), ".exe") {
return ""
}
return exe
}

// markBrowsers flags apps whose executable is a registered browser. Matches
// by full path first, then by basename — installs sometimes surface through
// a different discovery source (Start Menu vs App Paths) with an equivalent
// but not byte-identical path.
func markBrowsers(list []*AppData) {
idx := loadBrowserIndexOnce()
for _, app := range list {
if app == nil {
continue
}
// Recompute from the live registry index rather than trusting a flag
// persisted by an older cache (e.g. a browser uninstalled since).
app.IsBrowser = false
p := strings.Trim(strings.TrimSpace(app.AppPath), `"`)
if p == "" {
continue
}
p = filepath.Clean(p)
if idx.paths[normalizeKey(p)] || idx.basenames[strings.ToLower(filepath.Base(p))] {
app.IsBrowser = true
}
}
Comment thread
jigar-f marked this conversation as resolved.
}

func sortedKeys(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
Loading
Loading