Honor alternate autofill targets from entry fields
This commit is contained in:
Binary file not shown.
@@ -42,11 +42,11 @@ final class AutofillCacheStore {
|
|||||||
List<Entry> exactHost = new ArrayList<>();
|
List<Entry> exactHost = new ArrayList<>();
|
||||||
List<Entry> parentHost = new ArrayList<>();
|
List<Entry> parentHost = new ArrayList<>();
|
||||||
for (Entry entry : entries) {
|
for (Entry entry : entries) {
|
||||||
if (entry.host.equals(target.host)) {
|
if (entryMatchesHost(entry, target.host)) {
|
||||||
exactHost.add(entry);
|
exactHost.add(entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!entry.host.isEmpty() && target.host.endsWith("." + entry.host)) {
|
if (entryMatchesParentHost(entry, target.host)) {
|
||||||
parentHost.add(entry);
|
parentHost.add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +108,7 @@ final class AutofillCacheStore {
|
|||||||
String password = "";
|
String password = "";
|
||||||
String host = "";
|
String host = "";
|
||||||
String url = "";
|
String url = "";
|
||||||
|
List<String> targets = new ArrayList<>();
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
String name = reader.nextName();
|
String name = reader.nextName();
|
||||||
@@ -127,13 +128,20 @@ final class AutofillCacheStore {
|
|||||||
case "host":
|
case "host":
|
||||||
host = normalizeHost(nextString(reader));
|
host = normalizeHost(nextString(reader));
|
||||||
break;
|
break;
|
||||||
|
case "targets":
|
||||||
|
reader.beginArray();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
targets.add(nextString(reader));
|
||||||
|
}
|
||||||
|
reader.endArray();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reader.skipValue();
|
reader.skipValue();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
return new Entry(title, username, password, host, url);
|
return new Entry(title, username, password, host, url, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String nextString(JsonReader reader) throws IOException {
|
private static String nextString(JsonReader reader) throws IOException {
|
||||||
@@ -209,16 +217,21 @@ final class AutofillCacheStore {
|
|||||||
|
|
||||||
List<Entry> exact = new ArrayList<>();
|
List<Entry> exact = new ArrayList<>();
|
||||||
List<Entry> prefix = new ArrayList<>();
|
List<Entry> prefix = new ArrayList<>();
|
||||||
|
int bestPrefixLen = -1;
|
||||||
for (Entry entry : entries) {
|
for (Entry entry : entries) {
|
||||||
NormalizedTarget entryTarget = normalizeURL(entry.url);
|
MatchQuality quality = bestTargetMatch(entry, target);
|
||||||
if (entryTarget.host.isEmpty()) {
|
if (quality.exact) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (entryTarget.url.equals(target.url)) {
|
|
||||||
exact.add(entry);
|
exact.add(entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) {
|
if (quality.prefixLength <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (quality.prefixLength > bestPrefixLen) {
|
||||||
|
prefix.clear();
|
||||||
|
prefix.add(entry);
|
||||||
|
bestPrefixLen = quality.prefixLength;
|
||||||
|
} else if (quality.prefixLength == bestPrefixLen) {
|
||||||
prefix.add(entry);
|
prefix.add(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,23 +242,54 @@ final class AutofillCacheStore {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry best = null;
|
return prefix.size() == 1 ? prefix.get(0) : null;
|
||||||
int bestLen = -1;
|
}
|
||||||
boolean ambiguous = false;
|
|
||||||
for (Entry entry : prefix) {
|
private static boolean entryMatchesHost(Entry entry, String host) {
|
||||||
int pathLen = normalizeURL(entry.url).path.length();
|
for (NormalizedTarget target : entryTargets(entry)) {
|
||||||
if (pathLen > bestLen) {
|
if (target.host.equals(host)) {
|
||||||
best = entry;
|
return true;
|
||||||
bestLen = pathLen;
|
|
||||||
ambiguous = false;
|
|
||||||
} else if (pathLen == bestLen) {
|
|
||||||
ambiguous = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ambiguous) {
|
return false;
|
||||||
return null;
|
}
|
||||||
|
|
||||||
|
private static boolean entryMatchesParentHost(Entry entry, String host) {
|
||||||
|
for (NormalizedTarget target : entryTargets(entry)) {
|
||||||
|
if (!target.host.isEmpty() && host.endsWith("." + target.host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return best;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<NormalizedTarget> entryTargets(Entry entry) {
|
||||||
|
List<String> rawTargets = entry.targets;
|
||||||
|
if (rawTargets.isEmpty()) {
|
||||||
|
rawTargets = new ArrayList<>();
|
||||||
|
rawTargets.add(entry.url);
|
||||||
|
}
|
||||||
|
List<NormalizedTarget> targets = new ArrayList<>();
|
||||||
|
for (String rawTarget : rawTargets) {
|
||||||
|
NormalizedTarget target = normalizeURL(rawTarget);
|
||||||
|
if (!target.host.isEmpty()) {
|
||||||
|
targets.add(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MatchQuality bestTargetMatch(Entry entry, NormalizedTarget target) {
|
||||||
|
int bestPrefixLen = -1;
|
||||||
|
for (NormalizedTarget entryTarget : entryTargets(entry)) {
|
||||||
|
if (entryTarget.url.equals(target.url)) {
|
||||||
|
return new MatchQuality(true, 0);
|
||||||
|
}
|
||||||
|
if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) {
|
||||||
|
bestPrefixLen = Math.max(bestPrefixLen, entryTarget.path.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new MatchQuality(false, bestPrefixLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class Entry {
|
static final class Entry {
|
||||||
@@ -254,13 +298,25 @@ final class AutofillCacheStore {
|
|||||||
final String password;
|
final String password;
|
||||||
final String host;
|
final String host;
|
||||||
final String url;
|
final String url;
|
||||||
|
final List<String> targets;
|
||||||
|
|
||||||
Entry(String title, String username, String password, String host, String url) {
|
Entry(String title, String username, String password, String host, String url, List<String> targets) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
|
this.targets = new ArrayList<>(targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MatchQuality {
|
||||||
|
final boolean exact;
|
||||||
|
final int prefixLength;
|
||||||
|
|
||||||
|
MatchQuality(boolean exact, int prefixLength) {
|
||||||
|
this.exact = exact;
|
||||||
|
this.prefixLength = prefixLength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+111
-25
@@ -19,6 +19,7 @@ type Entry struct {
|
|||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
Targets []string `json:"targets,omitempty"`
|
||||||
Path []string `json:"path,omitempty"`
|
Path []string `json:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +37,11 @@ func Match(cache File, webURL string) (Entry, bool) {
|
|||||||
exactHost := make([]Entry, 0)
|
exactHost := make([]Entry, 0)
|
||||||
parentHost := make([]Entry, 0)
|
parentHost := make([]Entry, 0)
|
||||||
for _, entry := range cache.Entries {
|
for _, entry := range cache.Entries {
|
||||||
if entry.Host == target.host {
|
if entryMatchesHost(entry, target.host) {
|
||||||
exactHost = append(exactHost, entry)
|
exactHost = append(exactHost, entry)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if entry.Host != "" && strings.HasSuffix(target.host, "."+entry.Host) {
|
if entryMatchesParentHost(entry, target.host) {
|
||||||
parentHost = append(parentHost, entry)
|
parentHost = append(parentHost, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,16 @@ func Match(cache File, webURL string) (Entry, bool) {
|
|||||||
func Build(model vault.Model, now time.Time) File {
|
func Build(model vault.Model, now time.Time) File {
|
||||||
entries := make([]Entry, 0, len(model.Entries))
|
entries := make([]Entry, 0, len(model.Entries))
|
||||||
for _, item := range model.Entries {
|
for _, item := range model.Entries {
|
||||||
|
targets := collectTargets(item)
|
||||||
host := normalizeHost(item.URL)
|
host := normalizeHost(item.URL)
|
||||||
|
if host == "" {
|
||||||
|
for _, target := range targets {
|
||||||
|
host = normalizeHost(target)
|
||||||
|
if host != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if host == "" {
|
if host == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -68,6 +78,7 @@ func Build(model vault.Model, now time.Time) File {
|
|||||||
Password: item.Password,
|
Password: item.Password,
|
||||||
URL: item.URL,
|
URL: item.URL,
|
||||||
Host: host,
|
Host: host,
|
||||||
|
Targets: targets,
|
||||||
Path: append([]string(nil), item.Path...),
|
Path: append([]string(nil), item.Path...),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -150,18 +161,23 @@ func chooseEntry(target normalizedTarget, entries []Entry) (Entry, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exact := make([]Entry, 0)
|
exact := make([]Entry, 0)
|
||||||
prefix := make([]Entry, 0)
|
bestPrefixLen := -1
|
||||||
|
bestPrefix := make([]Entry, 0)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
entryTarget := normalizeURL(entry.URL)
|
exactMatch, prefixLen := bestTargetMatch(entry, target)
|
||||||
if entryTarget.host == "" {
|
if exactMatch {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if entryTarget.url == target.url {
|
|
||||||
exact = append(exact, entry)
|
exact = append(exact, entry)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if entryTarget.path != "/" && strings.HasPrefix(target.path, entryTarget.path) {
|
if prefixLen <= 0 {
|
||||||
prefix = append(prefix, entry)
|
continue
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case prefixLen > bestPrefixLen:
|
||||||
|
bestPrefixLen = prefixLen
|
||||||
|
bestPrefix = []Entry{entry}
|
||||||
|
case prefixLen == bestPrefixLen:
|
||||||
|
bestPrefix = append(bestPrefix, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(exact) == 1 {
|
if len(exact) == 1 {
|
||||||
@@ -170,22 +186,92 @@ func chooseEntry(target normalizedTarget, entries []Entry) (Entry, bool) {
|
|||||||
if len(exact) > 1 {
|
if len(exact) > 1 {
|
||||||
return Entry{}, false
|
return Entry{}, false
|
||||||
}
|
}
|
||||||
if len(prefix) == 0 {
|
if len(bestPrefix) == 1 {
|
||||||
|
return bestPrefix[0], true
|
||||||
|
}
|
||||||
|
if len(bestPrefix) == 0 {
|
||||||
return Entry{}, false
|
return Entry{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(prefix, func(i, j int) bool {
|
|
||||||
return len(normalizeURL(prefix[i].URL).path) > len(normalizeURL(prefix[j].URL).path)
|
|
||||||
})
|
|
||||||
bestPath := normalizeURL(prefix[0].URL).path
|
|
||||||
best := make([]Entry, 0, len(prefix))
|
|
||||||
for _, entry := range prefix {
|
|
||||||
if normalizeURL(entry.URL).path == bestPath {
|
|
||||||
best = append(best, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(best) == 1 {
|
|
||||||
return best[0], true
|
|
||||||
}
|
|
||||||
return Entry{}, false
|
return Entry{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectTargets(item vault.Entry) []string {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
targets := make([]string, 0, 1+len(item.Fields))
|
||||||
|
appendTarget := func(raw string) {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := seen[value]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
targets = append(targets, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTarget(item.URL)
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(item.Fields))
|
||||||
|
for key := range item.Fields {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, key := range keys {
|
||||||
|
upper := strings.ToUpper(strings.TrimSpace(key))
|
||||||
|
if strings.HasPrefix(upper, "ANDROIDAPP") || strings.HasPrefix(upper, "KP2A_URL") {
|
||||||
|
appendTarget(item.Fields[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryTargets(entry Entry) []normalizedTarget {
|
||||||
|
values := entry.Targets
|
||||||
|
if len(values) == 0 {
|
||||||
|
values = []string{entry.URL}
|
||||||
|
}
|
||||||
|
targets := make([]normalizedTarget, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
target := normalizeURL(value)
|
||||||
|
if target.host == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets = append(targets, target)
|
||||||
|
}
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryMatchesHost(entry Entry, host string) bool {
|
||||||
|
for _, target := range entryTargets(entry) {
|
||||||
|
if target.host == host {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryMatchesParentHost(entry Entry, host string) bool {
|
||||||
|
for _, target := range entryTargets(entry) {
|
||||||
|
if target.host != "" && strings.HasSuffix(host, "."+target.host) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func bestTargetMatch(entry Entry, target normalizedTarget) (bool, int) {
|
||||||
|
bestPrefixLen := -1
|
||||||
|
for _, candidate := range entryTargets(entry) {
|
||||||
|
if candidate.url == target.url {
|
||||||
|
return true, 0
|
||||||
|
}
|
||||||
|
if candidate.path != "/" && strings.HasPrefix(target.path, candidate.path) {
|
||||||
|
if pathLen := len(candidate.path); pathLen > bestPrefixLen {
|
||||||
|
bestPrefixLen = pathLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, bestPrefixLen
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) {
|
|||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "pass",
|
Password: "pass",
|
||||||
URL: "surveillance.crew.example.invalid",
|
URL: "surveillance.crew.example.invalid",
|
||||||
|
Fields: map[string]string{
|
||||||
|
"AndroidApp1": "androidapp://com.lights.mobile",
|
||||||
|
"KP2A_URL_1": "https://surveillance.crew.example.invalid/account",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, now)
|
}, now)
|
||||||
@@ -49,6 +53,9 @@ func TestBuildFiltersAndNormalizesEntries(t *testing.T) {
|
|||||||
if got.Entries[1].Host != "surveillance.crew.example.invalid" {
|
if got.Entries[1].Host != "surveillance.crew.example.invalid" {
|
||||||
t.Fatalf("second host = %q, want lights.julianfamily.org", got.Entries[1].Host)
|
t.Fatalf("second host = %q, want lights.julianfamily.org", got.Entries[1].Host)
|
||||||
}
|
}
|
||||||
|
if len(got.Entries[1].Targets) != 3 {
|
||||||
|
t.Fatalf("len(second targets) = %d, want 3", len(got.Entries[1].Targets))
|
||||||
|
}
|
||||||
if got.UpdatedAt != "2026-03-31T12:00:00Z" {
|
if got.UpdatedAt != "2026-03-31T12:00:00Z" {
|
||||||
t.Fatalf("updatedAt = %q", got.UpdatedAt)
|
t.Fatalf("updatedAt = %q", got.UpdatedAt)
|
||||||
}
|
}
|
||||||
@@ -235,3 +242,55 @@ func TestMatchRejectsAmbiguousAndroidAppPackageTargets(t *testing.T) {
|
|||||||
t.Fatalf("Match() unexpectedly resolved ambiguous android app package target")
|
t.Fatalf("Match() unexpectedly resolved ambiguous android app package target")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMatchUsesAndroidAppCustomFieldTarget(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cache := File{
|
||||||
|
Entries: []Entry{
|
||||||
|
{
|
||||||
|
ID: "one",
|
||||||
|
Title: "Blink",
|
||||||
|
Username: "blink-user",
|
||||||
|
Password: "secret1",
|
||||||
|
URL: "https://account.blinknetwork.com",
|
||||||
|
Host: "account.blinknetwork.com",
|
||||||
|
Targets: []string{"https://account.blinknetwork.com", "androidapp://com.blinknetwork.mobile2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := Match(cache, "androidapp://com.blinknetwork.mobile2")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Match() found no entry")
|
||||||
|
}
|
||||||
|
if got.ID != "one" {
|
||||||
|
t.Fatalf("Match() entry = %q, want one", got.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchUsesKP2AURLCustomFieldTarget(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cache := File{
|
||||||
|
Entries: []Entry{
|
||||||
|
{
|
||||||
|
ID: "one",
|
||||||
|
Title: "Blink",
|
||||||
|
Username: "blink-user",
|
||||||
|
Password: "secret1",
|
||||||
|
URL: "https://blinknetwork.com",
|
||||||
|
Host: "blinknetwork.com",
|
||||||
|
Targets: []string{"https://blinknetwork.com", "https://account.blinknetwork.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := Match(cache, "https://account.blinknetwork.com")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Match() found no entry")
|
||||||
|
}
|
||||||
|
if got.ID != "one" {
|
||||||
|
t.Fatalf("Match() entry = %q, want one", got.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user