iOS web browser with a focus on security and privacy
1/*
2 * Endless
3 * Copyright (c) 2015-2017 joshua stein <jcs@jcs.org>
4 *
5 * See LICENSE file for redistribution terms.
6 */
7
8#import "AppDelegate.h"
9#import "HostSettings.h"
10#import "HostSettingsController.h"
11
12#import "XLForm.h"
13
14@interface HostSettingsXLFormViewController : XLFormViewController
15@property (copy, nonatomic) void (^disappearCallback)(HostSettingsXLFormViewController *);
16@end
17
18@implementation HostSettingsXLFormViewController
19
20- (void)viewWillDisappear:(BOOL)animated
21{
22 if (self.disappearCallback)
23 self.disappearCallback(self);
24
25 [super viewWillDisappear:animated];
26}
27@end
28
29
30@implementation HostSettingsController {
31 AppDelegate *appDelegate;
32 NSMutableArray *_sortedHosts;
33 NSString *firstMatch;
34}
35
36- (void)viewDidLoad
37{
38 [super viewDidLoad];
39
40 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
41
42 self.title = NSLocalizedString(@"Host Settings", nil);
43 self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addHost:)];
44 self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", nil) style:UIBarButtonItemStyleDone target:self.navigationController action:@selector(dismissModalViewControllerAnimated:)];
45
46 /* most likely the user is wanting to define the site they are currently on, so feed that as a reasonable default the first time around */
47 if ([[appDelegate webViewController] curWebViewTab] != nil) {
48 NSURL *t = [[[appDelegate webViewController] curWebViewTab] url];
49 if (t != nil && [t host] != nil) {
50 NSRegularExpression *r = [NSRegularExpression regularExpressionWithPattern:@"^www\\." options:NSRegularExpressionCaseInsensitive error:nil];
51
52 firstMatch = [r stringByReplacingMatchesInString:[t host] options:0 range:NSMakeRange(0, [[t host] length]) withTemplate:@""];
53 }
54 }
55}
56
57- (void)didReceiveMemoryWarning
58{
59 [super didReceiveMemoryWarning];
60 // Dispose of any resources that can be recreated.
61}
62
63#pragma mark - Table view data source
64
65- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
66{
67 return 1;
68}
69
70- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
71{
72 return [[self sortedHosts] count];
73}
74
75- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
76{
77 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"host"];
78 if (cell == nil)
79 cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"host"];
80
81 cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
82
83 HostSettings *hs = [HostSettings forHost:[[self sortedHosts] objectAtIndex:indexPath.row]];
84 cell.textLabel.text = [hs hostname];
85 if ([hs isDefault])
86 cell.textLabel.font = [UIFont boldSystemFontOfSize:cell.textLabel.font.pointSize];
87 else
88 cell.detailTextLabel.text = nil;
89
90 return cell;
91}
92
93- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
94{
95 return (indexPath.row != 0);
96}
97
98- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
99{
100 [tableView deselectRowAtIndexPath:indexPath animated:YES];
101 [self showDetailsForHost:[[self sortedHosts] objectAtIndex:indexPath.row]];
102}
103
104- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
105{
106 if (editingStyle == UITableViewCellEditingStyleDelete) {
107 if ([HostSettings removeSettingsForHost:[[self sortedHosts] objectAtIndex:indexPath.row]]) {
108 [HostSettings persist];
109 _sortedHosts = nil;
110 [[self tableView] reloadData];
111 }
112 }
113}
114
115- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
116{
117 return NO;
118}
119
120- (void)addHost:sender
121{
122 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Host settings", nil) message:NSLocalizedString(@"Enter the host/domain to define settings for", nil) preferredStyle:UIAlertControllerStyleAlert];
123 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
124 textField.placeholder = @"example.com";
125
126 if (self->firstMatch != nil)
127 textField.text = self->firstMatch;
128 }];
129
130 UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK action") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
131 UITextField *host = alertController.textFields.firstObject;
132 if (host && ![[host text] isEqualToString:@""]) {
133 HostSettings *hs = [[HostSettings alloc] initForHost:[host text] withDict:nil];
134 [hs save];
135 [HostSettings persist];
136 self->_sortedHosts = nil;
137
138 [self.tableView reloadData];
139 [self showDetailsForHost:[host text]];
140 }
141 }];
142
143 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel action") style:UIAlertActionStyleCancel handler:nil];
144 [alertController addAction:cancelAction];
145 [alertController addAction:okAction];
146
147 [self presentViewController:alertController animated:YES completion:nil];
148
149 firstMatch = nil;
150}
151
152- (NSMutableArray *)sortedHosts
153{
154 if (_sortedHosts == nil)
155 _sortedHosts = [[NSMutableArray alloc] initWithArray:[HostSettings sortedHosts]];
156
157 return _sortedHosts;
158}
159
160- (void)showDetailsForHost:(NSString *)thost
161{
162 HostSettings *host = [HostSettings forHost:thost];
163
164 XLFormDescriptor *form = [XLFormDescriptor formDescriptorWithTitle:[host hostname]];
165
166 /* hostname */
167 {
168 XLFormSectionDescriptor *section = [XLFormSectionDescriptor formSection];
169 [form addFormSection:section];
170
171 if ([host isDefault]) {
172 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_HOST rowType:XLFormRowDescriptorTypeInfo title:NSLocalizedString(@"Host/domain", nil)];
173 [row setValue:HOST_SETTINGS_HOST_DEFAULT_LABEL];
174 [section setFooterTitle:NSLocalizedString(@"These settings will be used as defaults for all hosts unless overridden", nil)];
175 [section addFormRow:row];
176 }
177 else {
178 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_HOST rowType:XLFormRowDescriptorTypeText title:NSLocalizedString(@"Host/domain", nil)];
179 [row setValue:[host hostname]];
180 [row.cellConfigAtConfigure setObject:@"example.com" forKey:@"textField.placeholder"];
181 [row.cellConfigAtConfigure setObject:@(NSTextAlignmentRight) forKey:@"textField.textAlignment"];
182 [section setFooterTitle:NSLocalizedString(@"These settings will apply to this host and all hosts under it (e.g., \"example.com\" will apply to example.com and www.example.com)", nil)];
183 [section addFormRow:row];
184 }
185 }
186
187 /* privacy section */
188 {
189 XLFormSectionDescriptor *section = [XLFormSectionDescriptor formSection];
190 [section setTitle:NSLocalizedString(@"Privacy", nil)];
191 [form addFormSection:section];
192
193 /* whitelist cookies */
194 {
195 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_WHITELIST_COOKIES rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Allow persistent cookies", nil)];
196 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_WHITELIST_COOKIES host:host row:row withDefault:(![host isDefault])];
197
198 [section setFooterTitle:([host isDefault]
199 ? NSLocalizedString(@"Allow hosts to permanently store cookies and local storage databases", nil)
200 : NSLocalizedString(@"Allow this host to permanently store cookies and local storage databases", nil))
201 ];
202 [section addFormRow:row];
203 [form addFormSection:section];
204 }
205
206 /* allow webRTC */
207 {
208 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_ALLOW_WEBRTC rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Allow WebRTC", nil)];
209 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_ALLOW_WEBRTC host:host row:row withDefault:(![host isDefault])];
210
211 section = [XLFormSectionDescriptor formSection];
212 [section setTitle:@""];
213 [section setFooterTitle:([host isDefault]
214 ? NSLocalizedString(@"Allow hosts to access WebRTC functions", nil)
215 : NSLocalizedString(@"Allow this host to access WebRTC functions", nil))
216 ];
217 [section addFormRow:row];
218 [form addFormSection:section];
219 }
220
221 /* universal link protection */
222 {
223 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_UNIVERSAL_LINK_PROTECTION rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Universal Link Protection", nil)];
224 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_UNIVERSAL_LINK_PROTECTION host:host row:row withDefault:(![host isDefault])];
225
226 section = [XLFormSectionDescriptor formSection];
227 [section setTitle:@""];
228 [section setFooterTitle:([host isDefault]
229 ? NSLocalizedString(@"Handle tapping on links in a non-standard way to avoid possibly opening external applications", nil)
230 : NSLocalizedString(@"Handle tapping on links on pages from this host in a non-standard way to avoid possibly opening external applications", nil))
231 ];
232 [section addFormRow:row];
233 [form addFormSection:section];
234 }
235
236 }
237
238 /* security section */
239 {
240 XLFormSectionDescriptor *section = [XLFormSectionDescriptor formSection];
241 [section setTitle:NSLocalizedString(@"Security", nil)];
242 [form addFormSection:section];
243
244 /* ignore TLS errors */
245 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"allow_tls_error_ignore"])
246 {
247 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_IGNORE_TLS_ERRORS rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Ignore TLS errors", nil)];
248
249 XLFormOptionsObject *yes = [XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_VALUE_YES displayText:NSLocalizedString(@"Yes", nil)];
250 XLFormOptionsObject *no = [XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_VALUE_NO displayText:NSLocalizedString(@"No", nil)];
251
252 // This value is always "NO", except, when the user set the global setting
253 // "allow_tls_error_ignore" to YES *and* they surfed to a site with an error
254 // *and* the selected "ignore" on the following error alert.
255 [row setSelectorOptions:@[no]];
256
257 NSString *val = [host setting:HOST_SETTINGS_KEY_IGNORE_TLS_ERRORS];
258 [row setValue:[val isEqualToString:HOST_SETTINGS_VALUE_YES] ? yes : no];
259
260 [section addFormRow:row];
261 }
262
263 /* tls version */
264 {
265 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_TLS rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"TLS version", nil)];
266
267 NSMutableArray *opts = [[NSMutableArray alloc] init];
268 if (![host isDefault])
269 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_DEFAULT displayText:NSLocalizedString(@"(Use Default)", nil)]];
270 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_TLS_12 displayText:NSLocalizedString(@"TLS 1.2 Only", nil)]];
271 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_TLS_AUTO displayText:NSLocalizedString(@"TLS 1.2, 1.1, or 1.0", nil)]];
272 [row setSelectorOptions:opts];
273
274 NSString *val = [host setting:HOST_SETTINGS_KEY_TLS];
275 if (val == nil)
276 val = HOST_SETTINGS_DEFAULT;
277
278 for (XLFormOptionsObject *opt in opts)
279 if ([[opt valueData] isEqualToString:val])
280 [row setValue:opt];
281
282 [section setFooterTitle:([host isDefault]
283 ? NSLocalizedString(@"Minimum version of TLS required by hosts to negotiate HTTPS connections", nil)
284 : NSLocalizedString(@"Minimum version of TLS required by this host to negotiate HTTPS connections", nil))];
285 [section addFormRow:row];
286 }
287
288 /* content policy */
289 {
290 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_CSP rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Content policy", nil)];
291
292 NSMutableArray *opts = [[NSMutableArray alloc] init];
293 if (![host isDefault])
294 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_DEFAULT displayText:NSLocalizedString(@"(Use Default)", nil)]];
295 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_CSP_OPEN displayText:NSLocalizedString(@"Open (normal browsing mode)", nil)]];
296 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_CSP_BLOCK_CONNECT displayText:NSLocalizedString(@"No XHR/WebSocket/Video connections", nil)]];
297 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_CSP_STRICT displayText:NSLocalizedString(@"Strict (no JavaScript, video, etc.)", nil)]];
298 [row setSelectorOptions:opts];
299
300 NSString *val = [host setting:HOST_SETTINGS_KEY_CSP];
301 if (val == nil)
302 val = HOST_SETTINGS_DEFAULT;
303
304 for (XLFormOptionsObject *opt in opts)
305 if ([[opt valueData] isEqualToString:val])
306 [row setValue:opt];
307
308 section = [XLFormSectionDescriptor formSection];
309 [section setTitle:@""];
310 [section setFooterTitle:([host isDefault]
311 ? NSLocalizedString(@"Restrictions on resources loaded from web pages", nil)
312 : NSLocalizedString(@"Restrictions on resources loaded from web pages at this host", nil))];
313 [form addFormSection:section];
314 [section addFormRow:row];
315 }
316
317 /* whitelist cookies */
318 {
319 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_ALLOW_MIXED_MODE rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Allow mixed-mode resources", nil)];
320 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_ALLOW_MIXED_MODE host:host row:row withDefault:(![host isDefault])];
321
322 section = [XLFormSectionDescriptor formSection];
323 [section setTitle:@""];
324 [section setFooterTitle:([host isDefault]
325 ? NSLocalizedString(@"Allow HTTPS hosts to load page resources from non-HTTPS hosts (useful for RSS readers and other aggregators)", nil)
326 : NSLocalizedString(@"Allow this HTTPS host to load page resources from non-HTTPS hosts (useful for RSS readers and other aggregators)", nil))];
327 [form addFormSection:section];
328 [section addFormRow:row];
329 }
330
331 /* block external lan requests */
332 {
333 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS rowType:XLFormRowDescriptorTypeSelectorActionSheet title:NSLocalizedString(@"Block external LAN requests", nil)];
334 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS host:host row:row withDefault:(![host isDefault])];
335
336 section = [XLFormSectionDescriptor formSection];
337 [section setTitle:@""];
338 [section setFooterTitle:([host isDefault]
339 ? NSLocalizedString(@"Resources loaded from external hosts will be blocked from loading page elements or making requests to LAN hosts (192.168.0.0/16, 172.16.0.0/12, etc.)", nil)
340 : NSLocalizedString(@"Resources loaded from this host will be blocked from loading page elements or making requests to LAN hosts (192.168.0.0/16, 172.16.0.0/12, etc.)", nil))];
341 [form addFormSection:section];
342 [section addFormRow:row];
343 }
344 }
345
346 /* misc section */
347 {
348 XLFormSectionDescriptor *section = [XLFormSectionDescriptor formSection];
349 [section setTitle:NSLocalizedString(@"Other", nil)];
350 [form addFormSection:section];
351
352 /* user agent */
353 {
354 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_USER_AGENT rowType:XLFormRowDescriptorTypeText title:NSLocalizedString(@"User Agent", nil)];
355 [row setValue:[host setting:HOST_SETTINGS_KEY_USER_AGENT]];
356 [section setFooterTitle:[NSString stringWithFormat:NSLocalizedString(@"Custom user-agent string, or blank to use the default", nil)]];
357 [section addFormRow:row];
358 }
359 }
360
361 HostSettingsXLFormViewController *formController = [[HostSettingsXLFormViewController alloc] initWithForm:form];
362 [formController setDisappearCallback:^(HostSettingsXLFormViewController *form) {
363 if (![host isDefault])
364 [host setHostname:[[form formValues] objectForKey:HOST_SETTINGS_KEY_HOST]];
365
366 for (NSString *key in [[HostSettings defaults] allKeys]) {
367 XLFormOptionsObject *opt = [[form formValues] objectForKey:key];
368 if (opt)
369 [host setSetting:key toValue:(NSString *)[opt valueData]];
370 }
371
372 [host save];
373 [HostSettings persist];
374
375 dispatch_async(dispatch_get_main_queue(), ^{
376 [[NSNotificationCenter defaultCenter] postNotificationName:HOST_SETTINGS_CHANGED object:nil];
377 });
378 }];
379
380 [[self navigationController] pushViewController:formController animated:YES];
381 self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Hosts", nil) style:UIBarButtonItemStylePlain target:nil action:nil];
382}
383
384- (void)setYesNoSelectorOptionsForSetting:(NSString *)key host:(HostSettings *)host row:(XLFormRowDescriptor *)row withDefault:(BOOL)withDefault
385{
386 NSMutableArray *opts = [[NSMutableArray alloc] init];
387 if (withDefault)
388 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_DEFAULT displayText:NSLocalizedString(@"(Use Default)", nil)]];
389 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_VALUE_YES displayText:NSLocalizedString(@"Yes", no)]];
390 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_VALUE_NO displayText:NSLocalizedString(@"No", no)]];
391 [row setSelectorOptions:opts];
392
393 NSString *val = [host setting:key];
394 if (val == nil)
395 val = HOST_SETTINGS_DEFAULT;
396
397 for (XLFormOptionsObject *opt in opts)
398 if ([[opt valueData] isEqualToString:val])
399 [row setValue:opt];
400}
401
402@end