Move app packages under internal
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
package autofillcache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.julianfamily.org/keepassgo/internal/vault"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
URL string `json:"url"`
|
||||
Host string `json:"host"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
Path []string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Entries []Entry `json:"entries"`
|
||||
}
|
||||
|
||||
type MatchStatus string
|
||||
|
||||
const (
|
||||
MatchStatusNone MatchStatus = ""
|
||||
MatchStatusFound MatchStatus = "found"
|
||||
MatchStatusAmbiguous MatchStatus = "ambiguous"
|
||||
MatchStatusMissing MatchStatus = "missing"
|
||||
)
|
||||
|
||||
type MatchResult struct {
|
||||
Status MatchStatus `json:"status"`
|
||||
Entry Entry `json:"entry,omitempty"`
|
||||
}
|
||||
|
||||
func Match(cache File, webURL string) (Entry, bool) {
|
||||
result := Resolve(cache, webURL)
|
||||
return result.Entry, result.Status == MatchStatusFound
|
||||
}
|
||||
|
||||
func Resolve(cache File, webURL string) MatchResult {
|
||||
target := normalizeURL(webURL)
|
||||
if target.host == "" {
|
||||
return MatchResult{Status: MatchStatusMissing}
|
||||
}
|
||||
|
||||
exactHost := make([]Entry, 0)
|
||||
parentHost := make([]Entry, 0)
|
||||
for _, entry := range cache.Entries {
|
||||
if entryMatchesHost(entry, target.host) {
|
||||
exactHost = append(exactHost, entry)
|
||||
continue
|
||||
}
|
||||
if entryMatchesParentHost(entry, target.host) {
|
||||
parentHost = append(parentHost, entry)
|
||||
}
|
||||
}
|
||||
|
||||
if result := chooseEntry(target, exactHost); result.Status != MatchStatusMissing {
|
||||
return result
|
||||
}
|
||||
return chooseEntry(target, parentHost)
|
||||
}
|
||||
|
||||
func Build(model vault.Model, now time.Time) File {
|
||||
entries := make([]Entry, 0, len(model.Entries))
|
||||
for _, item := range model.Entries {
|
||||
targets := collectTargets(item)
|
||||
host := normalizeHost(item.URL)
|
||||
if host == "" {
|
||||
for _, target := range targets {
|
||||
host = normalizeHost(target)
|
||||
if host != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(item.Username) == "" || strings.TrimSpace(item.Password) == "" {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, Entry{
|
||||
ID: item.ID,
|
||||
Title: item.Title,
|
||||
Username: item.Username,
|
||||
Password: item.Password,
|
||||
URL: item.URL,
|
||||
Host: host,
|
||||
Targets: targets,
|
||||
Path: append([]string(nil), item.Path...),
|
||||
})
|
||||
}
|
||||
return File{
|
||||
UpdatedAt: now.UTC().Format(time.RFC3339),
|
||||
Entries: entries,
|
||||
}
|
||||
}
|
||||
|
||||
func Write(path string, model vault.Model, now time.Time) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(Build(model, now), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0o600)
|
||||
}
|
||||
|
||||
func Clear(path string) error {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeHost(raw string) string {
|
||||
return normalizeURL(raw).host
|
||||
}
|
||||
|
||||
type normalizedTarget struct {
|
||||
host string
|
||||
path string
|
||||
url string
|
||||
}
|
||||
|
||||
func normalizeURL(raw string) normalizedTarget {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return normalizedTarget{}
|
||||
}
|
||||
if !strings.Contains(value, "://") {
|
||||
value = "https://" + value
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return normalizedTarget{}
|
||||
}
|
||||
host := strings.TrimSpace(parsed.Hostname())
|
||||
path := cleanPath(parsed.EscapedPath())
|
||||
return normalizedTarget{
|
||||
host: strings.ToLower(host),
|
||||
path: path,
|
||||
url: strings.ToLower(host) + path,
|
||||
}
|
||||
}
|
||||
|
||||
func cleanPath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" || path == "/" {
|
||||
return "/"
|
||||
}
|
||||
path = strings.TrimRight(path, "/")
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func chooseEntry(target normalizedTarget, entries []Entry) MatchResult {
|
||||
switch len(entries) {
|
||||
case 0:
|
||||
return MatchResult{Status: MatchStatusMissing}
|
||||
case 1:
|
||||
return MatchResult{Status: MatchStatusFound, Entry: entries[0]}
|
||||
}
|
||||
|
||||
exact := make([]Entry, 0)
|
||||
bestPrefixLen := -1
|
||||
bestPrefix := make([]Entry, 0)
|
||||
for _, entry := range entries {
|
||||
exactMatch, prefixLen := bestTargetMatch(entry, target)
|
||||
if exactMatch {
|
||||
exact = append(exact, entry)
|
||||
continue
|
||||
}
|
||||
if prefixLen <= 0 {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case prefixLen > bestPrefixLen:
|
||||
bestPrefixLen = prefixLen
|
||||
bestPrefix = []Entry{entry}
|
||||
case prefixLen == bestPrefixLen:
|
||||
bestPrefix = append(bestPrefix, entry)
|
||||
}
|
||||
}
|
||||
if len(exact) == 1 {
|
||||
return MatchResult{Status: MatchStatusFound, Entry: exact[0]}
|
||||
}
|
||||
if len(exact) > 1 {
|
||||
return MatchResult{Status: MatchStatusAmbiguous}
|
||||
}
|
||||
if len(bestPrefix) == 1 {
|
||||
return MatchResult{Status: MatchStatusFound, Entry: bestPrefix[0]}
|
||||
}
|
||||
if len(bestPrefix) == 0 {
|
||||
return MatchResult{Status: MatchStatusMissing}
|
||||
}
|
||||
return MatchResult{Status: MatchStatusAmbiguous}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user