iOS web browser with a focus on security and privacy
at master 240 lines 6.8 kB view raw
1/* 2 * Endless 3 * Copyright (c) 2014-2015 joshua stein <jcs@jcs.org> 4 * 5 * See LICENSE file for redistribution terms. 6 */ 7 8#import "HTTPSEverywhere.h" 9 10@implementation HTTPSEverywhere 11 12static NSDictionary *_rules; 13static NSDictionary *_targets; 14static NSMutableDictionary *_disabledRules; 15static NSMutableDictionary *insecureRedirections; 16 17static NSCache *ruleCache; 18 19#define RULE_CACHE_SIZE 20 20 21+ (NSString *)disabledRulesPath 22{ 23 NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; 24 return [path stringByAppendingPathComponent:@"https_everywhere_disabled.plist"]; 25} 26 27+ (NSDictionary *)rules 28{ 29 if (_rules == nil) { 30 NSString *path = [[NSBundle mainBundle] pathForResource:@"https-everywhere_rules" ofType:@"plist"]; 31 if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { 32 NSLog(@"[HTTPSEverywhere] no rule plist at %@", path); 33 abort(); 34 } 35 36 _rules = [NSDictionary dictionaryWithContentsOfFile:path]; 37 38#ifdef TRACE_HTTPS_EVERYWHERE 39 NSLog(@"[HTTPSEverywhere] locked and loaded with %lu rules", [_rules count]); 40#endif 41 } 42 43 return _rules; 44} 45 46+ (NSMutableDictionary *)disabledRules 47{ 48 if (_disabledRules == nil) { 49 NSString *path = [[self class] disabledRulesPath]; 50 if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { 51 _disabledRules = [NSMutableDictionary dictionaryWithContentsOfFile:path]; 52 53#ifdef TRACE_HTTPS_EVERYWHERE 54 NSLog(@"[HTTPSEverywhere] loaded %lu disabled rules", [_disabledRules count]); 55#endif 56 } 57 else { 58 _disabledRules = [[NSMutableDictionary alloc] init]; 59 } 60 } 61 62 return _disabledRules; 63} 64 65+ (void)saveDisabledRules 66{ 67 [_disabledRules writeToFile:[[self class] disabledRulesPath] atomically:YES]; 68} 69 70+ (NSDictionary *)targets 71{ 72 if (_targets == nil) { 73 NSFileManager *fm = [NSFileManager defaultManager]; 74 NSString *path = [[NSBundle mainBundle] pathForResource:@"https-everywhere_targets" ofType:@"plist"]; 75 if (![fm fileExistsAtPath:path]) { 76 NSLog(@"[HTTPSEverywhere] no target plist at %@", path); 77 abort(); 78 } 79 80 _targets = [NSDictionary dictionaryWithContentsOfFile:path]; 81 82#ifdef TRACE_HTTPS_EVERYWHERE 83 NSLog(@"[HTTPSEverywhere] locked and loaded with %lu target domains", [_targets count]); 84#endif 85 } 86 87 return _targets; 88} 89 90+ (void)cacheRule:(HTTPSEverywhereRule *)rule forName:(NSString *)name 91{ 92 if (!ruleCache) { 93 ruleCache = [[NSCache alloc] init]; 94 [ruleCache setCountLimit:RULE_CACHE_SIZE]; 95 } 96 97#ifdef TRACE_HTTPS_EVERYWHERE 98 NSLog(@"[HTTPSEverywhere] cache miss for %@", name); 99#endif 100 101 [ruleCache setObject:rule forKey:name]; 102} 103 104+ (HTTPSEverywhereRule *)cachedRuleForName:(NSString *)name 105{ 106 HTTPSEverywhereRule *r; 107 108 if (ruleCache && (r = [ruleCache objectForKey:name]) != nil) { 109#ifdef TRACE_HTTPS_EVERYWHERE 110 NSLog(@"[HTTPSEverywhere] cache hit for %@", name); 111#endif 112 return r; 113 } 114 115 r = [[HTTPSEverywhereRule alloc] initWithDictionary:[[[self class] rules] objectForKey:name]]; 116 [[self class] cacheRule:r forName:name]; 117 118 return r; 119} 120 121+ (NSArray *)potentiallyApplicableRulesForHost:(NSString *)host 122{ 123 NSMutableDictionary *rs = [[NSMutableDictionary alloc] initWithCapacity:2]; 124 125 host = [host lowercaseString]; 126 127 NSString *targetName = [[[self class] targets] objectForKey:host]; 128 if (targetName != nil) 129 [rs setValue:[[self class] cachedRuleForName:targetName] forKey:targetName]; 130 131 /* now for x.y.z.example.com, try *.y.z.example.com, *.z.example.com, *.example.com, etc. */ 132 /* TODO: should we skip the last component for obviously non-matching things like "*.com", "*.net"? */ 133 NSArray *hostp = [host componentsSeparatedByString:@"."]; 134 for (int i = 1; i < [hostp count]; i++) { 135 NSString *wc = [NSString stringWithFormat:@"*.%@", [[hostp subarrayWithRange:NSMakeRange(i, [hostp count] - i)] componentsJoinedByString:@"."]]; 136 137 NSString *targetName = [[[self class] targets] objectForKey:wc]; 138 if (targetName != nil) { 139#ifdef TRACE_HTTPS_EVERYWHERE 140 NSLog(@"[HTTPSEverywhere] found ruleset %@ for component %@ in %@", targetName, wc, host); 141#endif 142 143 [rs setValue:[[self class] cachedRuleForName:targetName] forKey:targetName]; 144 } 145 } 146 147 return [rs allValues]; 148} 149 150+ (NSURL *)rewrittenURI:(NSURL *)URL withRules:(NSArray *)rules 151{ 152 if (rules == nil || [rules count] == 0) 153 rules = [[self class] potentiallyApplicableRulesForHost:[URL host]]; 154 155 if (rules == nil || [rules count] == 0) 156 return URL; 157 158#ifdef TRACE_HTTPS_EVERYWHERE 159 NSLog(@"[HTTPSEverywhere] have %lu applicable ruleset(s) for %@", [rules count], [URL absoluteString]); 160#endif 161 162 for (HTTPSEverywhereRule *rule in rules) { 163 if ([[HTTPSEverywhere disabledRules] valueForKey:[rule name]] != nil) 164 continue; 165 166 NSURL *rurl = [rule apply:URL]; 167 if (rurl != nil) 168 return rurl; 169 } 170 171 return URL; 172} 173 174+ (BOOL)needsSecureCookieFromHost:(NSString *)fromHost forHost:(NSString *)forHost cookieName:(NSString *)cookie 175{ 176 for (HTTPSEverywhereRule *rule in [[self class] potentiallyApplicableRulesForHost:fromHost]) { 177 if ([[HTTPSEverywhere disabledRules] valueForKey:[rule name]] != nil) 178 continue; 179 180 for (NSRegularExpression *hostreg in [rule secureCookies]) { 181 if ([hostreg matchesInString:forHost options:0 range:NSMakeRange(0, [forHost length])]) { 182 NSRegularExpression *namereg = [[rule secureCookies] objectForKey:hostreg]; 183 184 if ([namereg matchesInString:cookie options:0 range:NSMakeRange(0, [cookie length])]) { 185#ifdef TRACE_HTTPS_EVERYWHERE 186 NSLog(@"[HTTPSEverywhere] enabled securecookie for %@ from %@ for %@", cookie, fromHost, forHost); 187#endif 188 return YES; 189 } 190 } 191 } 192 } 193 194 return NO; 195} 196 197+ (void)noteInsecureRedirectionForURL:(NSURL *)URL toURL:(NSURL *)toURL 198{ 199 if (insecureRedirections == nil) { 200 insecureRedirections = [[NSMutableDictionary alloc] init]; 201 } 202 203 NSNumber *count = [insecureRedirections objectForKey:URL]; 204 if (count != nil && [count intValue] != 0) 205 count = [NSNumber numberWithInt:[count intValue] + 1]; 206 else 207 count = [NSNumber numberWithInt:1]; 208 209 [insecureRedirections setObject:count forKey:URL]; 210 211 if ([count intValue] < 3) { 212 return; 213 } 214 215 for (HTTPSEverywhereRule *rule in [[self class] potentiallyApplicableRulesForHost:[URL host]]) { 216 if ([rule apply:URL] != nil || [rule apply:toURL] != nil) { 217 NSLog(@"[HTTPSEverywhere] insecure redirection count %@ for %@, disabling rule %@", count, URL, [rule name]); 218 [[self class] disableRuleByName:[rule name] withReason:@"Redirection loop"]; 219 } 220 } 221} 222 223+ (BOOL)ruleNameIsDisabled:(NSString *)name 224{ 225 return ([[[self class] disabledRules] objectForKey:name] != nil); 226} 227 228+ (void)enableRuleByName:(NSString *)name 229{ 230 [[[self class] disabledRules] removeObjectForKey:name]; 231 [[self class] saveDisabledRules]; 232} 233 234+ (void)disableRuleByName:(NSString *)name withReason:(NSString *)reason 235{ 236 [[[self class] disabledRules] setObject:reason forKey:name]; 237 [[self class] saveDisabledRules]; 238} 239 240@end