From 5f2825c7411cc1c466de976fe74b2fb52b7c25e6 Mon Sep 17 00:00:00 2001 From: Christopher Luu Date: Sun, 20 Mar 2016 21:38:01 -0400 Subject: [PATCH] Update url matching to use levenshtein distance This update uses the levenshtein distance algorithm to determine the best matched entry, similar to how the KeePassHTTP plugin performs. This alleviates issues such as "www.facebook.com" not matching an entry whose URL is "facebook.com". --- MacPassHTTP.xcodeproj/project.pbxproj | 6 ++++ MacPassHTTP/MPHServerDelegate.m | 31 ++++++++++++++---- MacPassHTTP/NSString+Levenshtein.h | 15 +++++++++ MacPassHTTP/NSString+Levenshtein.m | 45 +++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 MacPassHTTP/NSString+Levenshtein.h create mode 100644 MacPassHTTP/NSString+Levenshtein.m diff --git a/MacPassHTTP.xcodeproj/project.pbxproj b/MacPassHTTP.xcodeproj/project.pbxproj index 73b40c8..4a27ad6 100644 --- a/MacPassHTTP.xcodeproj/project.pbxproj +++ b/MacPassHTTP.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 30D115AD1C9F74C900D98D00 /* NSString+Levenshtein.m in Sources */ = {isa = PBXBuildFile; fileRef = 30D115AC1C9F74C900D98D00 /* NSString+Levenshtein.m */; }; 4C10B73F1C08D2150077E477 /* MPHRequestAccessWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C10B73D1C08D2150077E477 /* MPHRequestAccessWindowController.m */; }; 4C3CAB051BFF339E009B4DF0 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4C3CAB071BFF339E009B4DF0 /* Localizable.strings */; }; 4C487E781BF3A0A400E595DE /* MPHMacPassHTTP.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C487E771BF3A0A400E595DE /* MPHMacPassHTTP.m */; }; @@ -32,6 +33,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 30D115AB1C9F74C900D98D00 /* NSString+Levenshtein.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Levenshtein.h"; sourceTree = ""; }; + 30D115AC1C9F74C900D98D00 /* NSString+Levenshtein.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Levenshtein.m"; sourceTree = ""; }; 4C10B73C1C08D2150077E477 /* MPHRequestAccessWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPHRequestAccessWindowController.h; sourceTree = ""; }; 4C10B73D1C08D2150077E477 /* MPHRequestAccessWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPHRequestAccessWindowController.m; sourceTree = ""; }; 4C3CAB011BFF32AB009B4DF0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MacPassHTTPSettings.strings; sourceTree = ""; }; @@ -106,6 +109,8 @@ 4C487E701BF39FC500E595DE /* Info.plist */, 4C967E341BF63CA900B1838C /* MPHServerDelegate.h */, 4C967E351BF63CA900B1838C /* MPHServerDelegate.m */, + 30D115AB1C9F74C900D98D00 /* NSString+Levenshtein.h */, + 30D115AC1C9F74C900D98D00 /* NSString+Levenshtein.m */, ); path = MacPassHTTP; sourceTree = ""; @@ -211,6 +216,7 @@ buildActionMask = 2147483647; files = ( 4C487E781BF3A0A400E595DE /* MPHMacPassHTTP.m in Sources */, + 30D115AD1C9F74C900D98D00 /* NSString+Levenshtein.m in Sources */, 4C967E361BF63CA900B1838C /* MPHServerDelegate.m in Sources */, 4C487E7C1BF3A47700E595DE /* MPHSettingsViewController.m in Sources */, 4C10B73F1C08D2150077E477 /* MPHRequestAccessWindowController.m in Sources */, diff --git a/MacPassHTTP/MPHServerDelegate.m b/MacPassHTTP/MPHServerDelegate.m index 568fb4c..1a936d7 100644 --- a/MacPassHTTP/MPHServerDelegate.m +++ b/MacPassHTTP/MPHServerDelegate.m @@ -12,6 +12,7 @@ #import "MPDocument.h" #import "NSString+MPPasswordCreation.h" +#import "NSString+Levenshtein.h" #import @@ -138,23 +139,41 @@ - (KPKEntry *)_createConfigurationEntry:(MPDocument *)document { #pragma mark - KPHDelegate -+ (NSArray *)recursivelyFindEntriesInGroups:(NSArray *)groups forURL:(NSString *)url { ++ (NSArray *)allEntriesInGroups:(NSArray *)groups { NSMutableArray *entries = @[].mutableCopy; for (KPKGroup *group in groups) { /* recurse through any subgroups */ - [entries addObjectsFromArray:[self recursivelyFindEntriesInGroups:group.groups forURL:url]]; + [entries addObjectsFromArray:[self allEntriesInGroups:group.groups]]; - /* check each entry in the group */ + /* add each entry in the group */ for (KPKEntry *entry in group.entries) { NSString *entryUrl = [entry.url finalValueForEntry:entry]; NSString *entryTitle = [entry.title finalValueForEntry:entry]; NSString *entryUsername = [entry.username finalValueForEntry:entry]; NSString *entryPassword = [entry.password finalValueForEntry:entry]; - if (url == nil || [entryTitle rangeOfString:url].length > 0 || [entryUrl rangeOfString:url].length > 0) { - [entries addObject:[KPHResponseEntry entryWithUrl:entryUrl name:entryTitle login:entryUsername password:entryPassword uuid:[entry.uuid UUIDString] stringFields:nil]]; + [entries addObject:[KPHResponseEntry entryWithUrl:entryUrl name:entryTitle login:entryUsername password:entryPassword uuid:[entry.uuid UUIDString] stringFields:nil]]; + } + } + return entries; +} + ++ (NSArray *)recursivelyFindEntriesInGroups:(NSArray *)groups forURL:(NSString *)url { + NSMutableArray *entries = @[].mutableCopy; + + NSUInteger minLevenshtein = NSUIntegerMax; + + /* iterate through each entry in the group to find the array of entries that have the minimum levenshtein distance */ + for (KPHResponseEntry *entry in [self allEntriesInGroups:groups]) { + NSUInteger levenshtein = [url levenshteinDistanceToString:entry.url]; + if (url == nil || ([[entry.url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] > 0 && levenshtein <= minLevenshtein)) { + /* if we have found a new minimum levenshtein distance, remove all previous entries */ + if (url != nil && levenshtein < minLevenshtein) { + minLevenshtein = levenshtein; + [entries removeAllObjects]; } + [entries addObject:entry]; } } return entries; @@ -246,7 +265,7 @@ - (NSArray *)allEntriesForServer:(KPHServer *)server { return @[]; } - return [MPHServerDelegate recursivelyFindEntriesInGroups:self.queryDocument.root.groups forURL:nil]; + return [MPHServerDelegate allEntriesInGroups:@[self.queryDocument.root]]; } - (NSString *)generatePasswordForServer:(KPHServer *)server { diff --git a/MacPassHTTP/NSString+Levenshtein.h b/MacPassHTTP/NSString+Levenshtein.h new file mode 100644 index 0000000..03003b5 --- /dev/null +++ b/MacPassHTTP/NSString+Levenshtein.h @@ -0,0 +1,15 @@ +// +// NSString+Levenshtein.h +// MacPassHTTP +// +// Created by Christopher Luu on 20/03/15. +// Copyright © 2015 HicknHack Software GmbH. All rights reserved. +// + +#import + +@interface NSString (Levenshtein) + +- (NSUInteger)levenshteinDistanceToString:(NSString *)string; + +@end diff --git a/MacPassHTTP/NSString+Levenshtein.m b/MacPassHTTP/NSString+Levenshtein.m new file mode 100644 index 0000000..c6e6b63 --- /dev/null +++ b/MacPassHTTP/NSString+Levenshtein.m @@ -0,0 +1,45 @@ +// +// NSString+Levenshtein.m +// MacPassHTTP +// +// Created by Christopher Luu on 20/03/15. +// Copyright © 2015 HicknHack Software GmbH. All rights reserved. +// + +#import "NSString+Levenshtein.h" +#include + +@implementation NSString (Levenshtein) + +// This implementation can be found here: https://rosettacode.org/wiki/Levenshtein_distance#Objective-C +- (NSUInteger)levenshteinDistanceToString:(NSString *)string { + NSUInteger sl = [self length]; + NSUInteger tl = [string length]; + NSUInteger *d = calloc(sizeof(*d), (sl+1) * (tl+1)); + +#define d(i, j) d[((j) * sl) + (i)] + for (NSUInteger i = 0; i <= sl; i++) { + d(i, 0) = i; + } + for (NSUInteger j = 0; j <= tl; j++) { + d(0, j) = j; + } + for (NSUInteger j = 1; j <= tl; j++) { + for (NSUInteger i = 1; i <= sl; i++) { + if ([self characterAtIndex:i-1] == [string characterAtIndex:j-1]) { + d(i, j) = d(i-1, j-1); + } else { + d(i, j) = MIN(d(i-1, j), MIN(d(i, j-1), d(i-1, j-1))) + 1; + } + } + } + + NSUInteger r = d(sl, tl); +#undef d + + free(d); + + return r; +} + +@end