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 "URLBlocker.h"
9
10@implementation URLBlocker
11
12static NSDictionary *_targets;
13static NSMutableDictionary *_disabledTargets;
14static NSCache *ruleCache;
15
16#define RULE_CACHE_SIZE 20
17
18+ (NSString *)disabledTargetsPath
19{
20 NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
21 return [path stringByAppendingPathComponent:@"url_blocker_disabled.plist"];
22}
23
24+ (NSDictionary *)targets
25{
26 NSError *error;
27
28 if (_targets == nil) {
29 NSString *path = [[NSBundle mainBundle] pathForResource:@"urlblocker" ofType:@"json"];
30 if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
31 NSLog(@"[URLBlocker] no target plist at %@", path);
32 abort();
33 }
34
35 NSInputStream *input = [[NSInputStream alloc] initWithFileAtPath:path];
36 if (input != nil) {
37 [input open];
38
39 NSDictionary *blockers = [NSJSONSerialization JSONObjectWithStream:input options:kNilOptions error:&error];
40 if (error != nil)
41 NSLog(@"[URLBlocker] couldn't read %@: %@", path, error);
42 [input close];
43
44 /* convert from { "desc" => [ "host1", "host2" ] } to { "host1" => "desc", "host2" => "desc" } */
45 NSMutableDictionary *ttargets = [[NSMutableDictionary alloc] init];
46 for (NSString *key in [blockers allKeys]) {
47 NSArray *doms = [blockers objectForKey:key];
48 for (NSString *dom in doms)
49 [ttargets setObject:key forKey:dom];
50 }
51
52 _targets = [NSDictionary dictionaryWithDictionary:ttargets];
53 }
54
55 if (!_targets || ![_targets count]) {
56 NSLog(@"[URLBlocker] couldn't read %@", path);
57 _targets = @{};
58 return _targets;
59 }
60
61#ifdef TRACE_URL_BLOCKER
62 NSLog(@"[URLBlocker] locked and loaded with %lu target domains", [_targets count]);
63#endif
64 }
65
66 return _targets;
67}
68
69+ (NSMutableDictionary *)disabledTargets
70{
71 if (_disabledTargets == nil) {
72 NSString *path = [[self class] disabledTargetsPath];
73 if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
74 _disabledTargets = [NSMutableDictionary dictionaryWithContentsOfFile:path];
75
76#ifdef TRACE_HTTPS_EVERYWHERE
77 NSLog(@"[URLBlocker] loaded %lu disabled targets", [_disabledTargets count]);
78#endif
79 }
80 else {
81 _disabledTargets = [[NSMutableDictionary alloc] init];
82 }
83 }
84
85 return _disabledTargets;
86}
87
88+ (void)saveDisabledTargets
89{
90 [_disabledTargets writeToFile:[[self class] disabledTargetsPath] atomically:YES];
91}
92
93+ (void)cacheBlockedURL:(NSURL *)url withRule:(NSString *)rule
94{
95 if (!ruleCache) {
96 ruleCache = [[NSCache alloc] init];
97 [ruleCache setCountLimit:RULE_CACHE_SIZE];
98 }
99
100 [ruleCache setObject:rule forKey:url];
101}
102
103+ (NSString *)blockRuleForURL:(NSURL *)url
104{
105 NSString *blocker;
106
107 if (!(ruleCache && (blocker = [ruleCache objectForKey:url]))) {
108 NSString *host = [[url host] lowercaseString];
109
110 if ([[[self class] targets] objectForKey:host])
111 blocker = host;
112 else {
113 /* now for x.y.z.example.com, try *.y.z.example.com, *.z.example.com, *.example.com, etc. */
114 /* TODO: should we skip the last component for obviously non-matching things like "*.com", "*.net"? */
115 NSArray *hostp = [host componentsSeparatedByString:@"."];
116 for (int i = 1; i < [hostp count]; i++) {
117 NSString *wc = [[hostp subarrayWithRange:NSMakeRange(i, [hostp count] - i)] componentsJoinedByString:@"."];
118
119 if ([[[self class] targets] objectForKey:wc]) {
120 blocker = wc;
121 break;
122 }
123 }
124 }
125 }
126
127 if (blocker && [[URLBlocker disabledTargets] objectForKey:blocker] != nil)
128 return nil;
129
130 if (blocker)
131 [[self class] cacheBlockedURL:url withRule:blocker];
132
133 return blocker;
134}
135
136+ (BOOL)shouldBlockURL:(NSURL *)url
137{
138 return ([self blockRuleForURL:url] != nil);
139}
140
141+ (NSString *)blockingTargetForURL:(NSURL *)url fromMainDocumentURL:(NSURL *)mainUrl
142{
143 NSString *blocker = [self blockRuleForURL:url];
144 if (blocker != nil && mainUrl != nil) {
145 /* if this same rule would have blocked our main URL, allow it since the user is probably viewing this site and this isn't a sneaky tracker */
146 if ([blocker isEqualToString:[self blockRuleForURL:mainUrl]]) {
147 return nil;
148 }
149
150#ifdef TRACE_URL_BLOCKER
151 NSLog(@"[URLBlocker] blocking %@ (via %@) (%@)", url, mainUrl, blocker);
152#endif
153
154 return blocker;
155 }
156
157 return nil;
158}
159
160+ (void)enableTargetByHost:(NSString *)target
161{
162 [[[self class] disabledTargets] removeObjectForKey:target];
163 [[self class] saveDisabledTargets];
164}
165
166+ (void)disableTargetByHost:(NSString *)target withReason:(NSString *)reason
167{
168 [[[self class] disabledTargets] setObject:reason forKey:target];
169 [[self class] saveDisabledTargets];
170}
171
172@end