Skip to content
This repository was archived by the owner on May 27, 2019. It is now read-only.

Commit 30c9da4

Browse files
sophaclesmax-baz
authored andcommitted
Implement manual fuzzy search, enabled by default (#213, fixes #32)
1 parent 9d367c0 commit 30c9da4

File tree

9 files changed

+194
-30
lines changed

9 files changed

+194
-30
lines changed

Gopkg.lock

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browserpass.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@ type Login struct {
2727

2828
var endianness = binary.LittleEndian
2929

30+
// Settings info for the browserpass program.
31+
//
32+
// The browser extension will look up settings in its localstorage and find
33+
// which options have been selected by the user, and put them in a JSON object
34+
// which is then passed along with the command over the native messaging api.
35+
type Config struct {
36+
// Manual searches use FuzzySearch if true, GlobSearch otherwise
37+
UseFuzzy bool `json:"use_fuzzy_search"`
38+
}
39+
3040
// msg defines a message sent from a browser extension.
3141
type msg struct {
32-
Action string `json:"action"`
33-
Domain string `json:"domain"`
34-
Entry string `json:"entry"`
42+
Settings Config `json:"settings"`
43+
Action string `json:"action"`
44+
Domain string `json:"domain"`
45+
Entry string `json:"entry"`
3546
}
3647

3748
// Run starts browserpass.
@@ -53,6 +64,10 @@ func Run(stdin io.Reader, stdout io.Writer, s pass.Store) error {
5364
return err
5465
}
5566

67+
// Since the pass.Store object is created by the wrapper prior to
68+
// settings from the browser being made available, we set them here
69+
s.SetConfig(&data.Settings.UseFuzzy)
70+
5671
var resp interface{}
5772
switch data.Action {
5873
case "search":
@@ -61,6 +76,12 @@ func Run(stdin io.Reader, stdout io.Writer, s pass.Store) error {
6176
return err
6277
}
6378
resp = list
79+
case "match_domain":
80+
list, err := s.GlobSearch(data.Domain)
81+
if err != nil {
82+
return err
83+
}
84+
resp = list
6485
case "get":
6586
rc, err := s.Open(data.Entry)
6687
if err != nil {

chrome/background.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ function onMessage(request, sender, sendResponse) {
8585
if (request.action == "dismissOTP" && sender.tab.id in tabInfos) {
8686
delete tabInfos[sender.tab.id];
8787
}
88+
89+
// allows the local communication to request settings. Returns an
90+
// object that has current settings. Update this as new settings
91+
// are added (or old ones removed)
92+
if (request.action == "getSettings") {
93+
const use_fuzzy_search = localStorage.getItem("use_fuzzy_search") != "false";
94+
sendResponse({ use_fuzzy_search: use_fuzzy_search})
95+
}
8896
}
8997

9098
function onTabUpdated(tabId, changeInfo, tab) {

chrome/options.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
Auto-Submit login forms
1414
</label>
1515
<br/>
16+
<label>
17+
<input type="checkbox" id="use-fuzzy">
18+
Use fuzzy search
19+
</label>
20+
<br/>
1621
<br/>
1722
<button id="save">Save</button>
1823

chrome/options.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
function save_options() {
22
var autoSubmit = document.getElementById("auto-submit").checked;
33
localStorage.setItem("autoSubmit", autoSubmit);
4+
5+
// Options related to fuzzy finding.
6+
// use_fuzzy_search indicates if fuzzy finding or glob searching should
7+
// be used in manual searches
8+
var use_fuzzy = document.getElementById("use-fuzzy").checked;
9+
localStorage.setItem("use_fuzzy_search", use_fuzzy);
10+
411
window.close();
512
}
613

714
function restore_options() {
815
var autoSubmit = localStorage.getItem("autoSubmit") == "true";
916
document.getElementById("auto-submit").checked = autoSubmit;
17+
18+
// Restore the view to show the settings described above
19+
var use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false";
20+
document.getElementById("use-fuzzy").checked = use_fuzzy;
1021
}
1122

1223
document.addEventListener("DOMContentLoaded", restore_options);

chrome/script.browserify.js

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,30 +105,38 @@ function init(tab) {
105105

106106
activeTab = tab;
107107
var activeDomain = parseDomainFromUrl(tab.url);
108-
searchPassword(activeDomain);
108+
searchPassword(activeDomain, "match_domain");
109109
}
110110

111-
function searchPassword(_domain) {
111+
function searchPassword(_domain, action="search") {
112112
searching = true;
113113
logins = null;
114114
domain = _domain;
115115
urlDuringSearch = activeTab.url;
116116
m.redraw();
117117

118-
chrome.runtime.sendNativeMessage(
119-
app,
120-
{ action: "search", domain: _domain },
118+
// First get the settings needed by the browserpass native client
119+
// by requesting them from the background script (which has localStorage access
120+
// to the settings). Then construct the message to send to browserpass and
121+
// send that via sendNativeMessage.
122+
chrome.runtime.sendMessage(
123+
{ action: "getSettings" },
121124
function(response) {
122-
if (chrome.runtime.lastError) {
123-
error = chrome.runtime.lastError.message;
124-
console.error(error);
125-
}
125+
chrome.runtime.sendNativeMessage(
126+
app,
127+
{ action: action, domain: _domain, settings: response},
128+
function(response) {
129+
if (chrome.runtime.lastError) {
130+
error = chrome.runtime.lastError.message;
131+
console.error(error);
132+
}
126133

127-
searching = false;
128-
logins = response;
129-
m.redraw();
130-
}
131-
);
134+
searching = false;
135+
logins = response;
136+
m.redraw();
137+
}
138+
);
139+
});
132140
}
133141

134142
function parseDomainFromUrl(url) {

pass/disk.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111
"os/user"
1212

1313
"github.com/mattn/go-zglob"
14+
sfuzzy "github.com/sahilm/fuzzy"
1415
)
1516

1617
type diskStore struct {
17-
path string
18+
path string
19+
useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches
1820
}
1921

2022
func NewDefaultStore() (Store, error) {
@@ -23,7 +25,7 @@ func NewDefaultStore() (Store, error) {
2325
return nil, err
2426
}
2527

26-
return &diskStore{path}, nil
28+
return &diskStore{path, false}, nil
2729
}
2830

2931
func defaultStorePath() (string, error) {
@@ -42,7 +44,47 @@ func defaultStorePath() (string, error) {
4244
return filepath.EvalSymlinks(path)
4345
}
4446

47+
// Set the configuration options for password matching.
48+
func (s *diskStore) SetConfig(use_fuzzy *bool) error {
49+
if use_fuzzy != nil {
50+
s.useFuzzy = *use_fuzzy
51+
}
52+
return nil
53+
}
54+
55+
// Do a search. Will call into the correct algoritm (glob or fuzzy)
56+
// based on the settings present in the diskStore struct
4557
func (s *diskStore) Search(query string) ([]string, error) {
58+
// default glob search
59+
if !s.useFuzzy {
60+
return s.GlobSearch(query)
61+
} else {
62+
return s.FuzzySearch(query)
63+
}
64+
}
65+
66+
// Fuzzy searches first get a list of all pass entries by doing a glob search
67+
// for the empty string, then apply appropriate logic to convert results to
68+
// a slice of strings, finally returning all of the unique entries.
69+
func (s *diskStore) FuzzySearch(query string) ([]string, error) {
70+
var items []string
71+
file_list, err := s.GlobSearch("")
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
// The resulting match struct does not copy the strings, but rather
77+
// provides the index to the original array. Copy those strings
78+
// into the result slice
79+
matches := sfuzzy.Find(query, file_list)
80+
for _, match := range matches {
81+
items = append(items, file_list[match.Index])
82+
}
83+
84+
return items, nil
85+
}
86+
87+
func (s *diskStore) GlobSearch(query string) ([]string, error) {
4688
// Search:
4789
// 1. DOMAIN/USERNAME.gpg
4890
// 2. DOMAIN.gpg
@@ -69,7 +111,7 @@ func (s *diskStore) Search(query string) ([]string, error) {
69111
if strings.Count(query, ".") >= 2 {
70112
// try finding additional items by removing subparts of the query
71113
queryParts := strings.SplitN(query, ".", 2)[1:]
72-
newItems, err := s.Search(strings.Join(queryParts, "."))
114+
newItems, err := s.GlobSearch(strings.Join(queryParts, "."))
73115
if err != nil {
74116
return nil, err
75117
}

pass/disk_test.go

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestDefaultStorePath(t *testing.T) {
2424
expected = filepath.Join(home, ".password-store")
2525
actual, _ = defaultStorePath()
2626
if expected != actual {
27-
t.Errorf("%s does not match %s", expected, actual)
27+
t.Errorf("1: '%s' does not match '%s'", expected, actual)
2828
}
2929

3030
// custom directory from $PASSWORD_STORE_DIR
@@ -36,9 +36,12 @@ func TestDefaultStorePath(t *testing.T) {
3636
fmt.Println(expected)
3737
os.Mkdir(expected, os.ModePerm)
3838
os.Setenv("PASSWORD_STORE_DIR", expected)
39-
actual, _ = defaultStorePath()
39+
actual, err = defaultStorePath()
40+
if err != nil {
41+
t.Error(err)
42+
}
4043
if expected != actual {
41-
t.Errorf("%s does not match %s", expected, actual)
44+
t.Errorf("2: '%s' does not match '%s'", expected, actual)
4245
}
4346

4447
// clean-up
@@ -63,7 +66,7 @@ func TestDiskStore_Search_nomatch(t *testing.T) {
6366
}
6467

6568
func TestDiskStoreSearch(t *testing.T) {
66-
store := diskStore{"test_store"}
69+
store := diskStore{"test_store", false}
6770
targetDomain := "abc.com"
6871
testDomains := []string{"abc.com", "test.abc.com", "testing.test.abc.com"}
6972
for _, domain := range testDomains {
@@ -86,7 +89,7 @@ func TestDiskStoreSearch(t *testing.T) {
8689
}
8790

8891
func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testing.T) {
89-
store := diskStore{"test_store"}
92+
store := diskStore{"test_store", false}
9093
searchResult, err := store.Search("xyz")
9194
if err != nil {
9295
t.Fatal(err)
@@ -101,7 +104,7 @@ func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testi
101104
}
102105

103106
func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) {
104-
store := diskStore{"test_store"}
107+
store := diskStore{"test_store", false}
105108
searchResult, err := store.Search("def.com")
106109
if err != nil {
107110
t.Fatal(err)
@@ -116,7 +119,7 @@ func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) {
116119
}
117120

118121
func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) {
119-
store := diskStore{"test_store"}
122+
store := diskStore{"test_store", false}
120123
searchResult, err := store.Search("amazon.co.uk")
121124
if err != nil {
122125
t.Fatal(err)
@@ -131,7 +134,7 @@ func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) {
131134
}
132135

133136
func TestDiskStoreSearchSubDirectories(t *testing.T) {
134-
store := diskStore{"test_store"}
137+
store := diskStore{"test_store", false}
135138
searchTermsMatches := map[string][]string{
136139
"abc.org": []string{"abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"},
137140
"wiki": []string{"abc.org/wiki/user4", "abc.org/wiki/work/user5"},
@@ -155,7 +158,7 @@ func TestDiskStoreSearchSubDirectories(t *testing.T) {
155158
}
156159

157160
func TestDiskStorePartSearch(t *testing.T) {
158-
store := diskStore{"test_store"}
161+
store := diskStore{"test_store", false}
159162
searchResult, err := store.Search("ab")
160163
if err != nil {
161164
t.Fatal(err)
@@ -170,3 +173,60 @@ func TestDiskStorePartSearch(t *testing.T) {
170173
}
171174
}
172175
}
176+
177+
func TestFuzzySearch(t *testing.T) {
178+
store := diskStore{"test_store", true}
179+
searchResult, err := store.Search("amaz2")
180+
181+
if err != nil {
182+
t.Fatal(err)
183+
}
184+
if len(searchResult) != 2 {
185+
t.Fatalf("Result size was: %s expected 2", len(searchResult))
186+
}
187+
188+
expectedResult := map[string]bool{
189+
"amazon.co.uk/user2": true,
190+
"amazon.com/user2": true,
191+
}
192+
193+
for _, res := range searchResult {
194+
if !expectedResult[res] {
195+
t.Fatalf("Result %s not expected!", res)
196+
}
197+
}
198+
}
199+
200+
func TestFuzzySearchNoResult(t *testing.T) {
201+
store := diskStore{"test_store", true}
202+
searchResult, err := store.Search("vvv")
203+
204+
if err != nil {
205+
t.Fatal(err)
206+
}
207+
if len(searchResult) != 0 {
208+
t.Fatalf("Result size was: %s expected 0", len(searchResult))
209+
}
210+
}
211+
212+
func TestFuzzySearchTopLevelEntries(t *testing.T) {
213+
store := diskStore{"test_store", true}
214+
searchResult, err := store.Search("def")
215+
216+
if err != nil {
217+
t.Fatal(err)
218+
}
219+
if len(searchResult) != 1 {
220+
t.Fatalf("Result size was: %s expected 1", len(searchResult))
221+
}
222+
223+
expectedResult := map[string]bool{
224+
"def.com": true,
225+
}
226+
227+
for _, res := range searchResult {
228+
if !expectedResult[res] {
229+
t.Fatalf("Result %s not expected!", res)
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)