iOS web browser with a focus on security and privacy
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