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 = @"Host Settings";
43 self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addHost:)];
44 self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Done" 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:@"Host settings" message:@"Enter the host/domain to define settings for" preferredStyle:UIAlertControllerStyleAlert];
123 [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
124 textField.placeholder = @"example.com";
125
126 if (firstMatch != nil)
127 textField.text = 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 _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:@"Host/domain"];
173 [row setValue:HOST_SETTINGS_HOST_DEFAULT_LABEL];
174 [section setFooterTitle:@"These settings will be used as defaults for all hosts unless overridden"];
175 [section addFormRow:row];
176 }
177 else {
178 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_HOST rowType:XLFormRowDescriptorTypeText title:@"Host/domain"];
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:@"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)"];
183 [section addFormRow:row];
184 }
185 }
186
187 /* privacy section */
188 {
189 XLFormSectionDescriptor *section = [XLFormSectionDescriptor formSection];
190 [section setTitle:@"Privacy"];
191 [form addFormSection:section];
192
193 /* whitelist cookies */
194 {
195 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_WHITELIST_COOKIES rowType:XLFormRowDescriptorTypeSelectorActionSheet title:@"Allow persistent cookies"];
196 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_WHITELIST_COOKIES host:host row:row withDefault:(![host isDefault])];
197
198 [section setFooterTitle:[NSString stringWithFormat:@"Allow %@ to permanently store cookies and local storage databases", ([host isDefault] ? @"hosts" : @"this host")]];
199 [section addFormRow:row];
200 }
201 }
202
203 /* security section */
204 {
205 XLFormSectionDescriptor *section = [XLFormSectionDescriptor formSection];
206 [section setTitle:@"Security"];
207 [form addFormSection:section];
208
209 /* tls version */
210 {
211 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_TLS rowType:XLFormRowDescriptorTypeSelectorActionSheet title:@"TLS version"];
212
213 NSMutableArray *opts = [[NSMutableArray alloc] init];
214 if (![host isDefault])
215 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_DEFAULT displayText:@"(Use Default)"]];
216 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_TLS_12 displayText:@"TLS 1.2 Only"]];
217 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_TLS_AUTO displayText:@"TLS 1.2, 1.1, or 1.0"]];
218 [row setSelectorOptions:opts];
219
220 NSString *val = [host setting:HOST_SETTINGS_KEY_TLS];
221 if (val == nil)
222 val = HOST_SETTINGS_DEFAULT;
223
224 for (XLFormOptionsObject *opt in opts)
225 if ([[opt valueData] isEqualToString:val])
226 [row setValue:opt];
227
228 [section setFooterTitle:[NSString stringWithFormat:@"Minimum version of TLS required by %@ to negotiate HTTPS connections", ([host isDefault] ? @"hosts" : @"this host")]];
229 [section addFormRow:row];
230 }
231
232 /* content policy */
233 {
234 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_CSP rowType:XLFormRowDescriptorTypeSelectorActionSheet title:@"Content policy"];
235
236 NSMutableArray *opts = [[NSMutableArray alloc] init];
237 if (![host isDefault])
238 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_DEFAULT displayText:@"(Use Default)"]];
239 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_CSP_OPEN displayText:@"Open (normal browsing mode)"]];
240 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_CSP_BLOCK_CONNECT displayText:@"No XHR/WebSockets/Video connections"]];
241 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_CSP_STRICT displayText:@"Strict (no JavaScript, video, etc.)"]];
242 [row setSelectorOptions:opts];
243
244 NSString *val = [host setting:HOST_SETTINGS_KEY_CSP];
245 if (val == nil)
246 val = HOST_SETTINGS_DEFAULT;
247
248 for (XLFormOptionsObject *opt in opts)
249 if ([[opt valueData] isEqualToString:val])
250 [row setValue:opt];
251
252 section = [XLFormSectionDescriptor formSection];
253 [section setTitle:@""];
254 [section setFooterTitle:[NSString stringWithFormat:@"Restrictions on resources loaded from web pages%@", ([host isDefault] ? @"" : @" at this host")]];
255 [form addFormSection:section];
256 [section addFormRow:row];
257 }
258
259 /* whitelist cookies */
260 {
261 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_ALLOW_MIXED_MODE rowType:XLFormRowDescriptorTypeSelectorActionSheet title:@"Allow mixed-mode resources"];
262 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_ALLOW_MIXED_MODE host:host row:row withDefault:(![host isDefault])];
263
264 section = [XLFormSectionDescriptor formSection];
265 [section setTitle:@""];
266 [section setFooterTitle:[NSString stringWithFormat:@"Allow %@ to load page resources from non-HTTPS hosts (useful for RSS readers and other aggregators)", ([host isDefault] ? @"HTTPS hosts" : @"this HTTPS host")]];
267 [form addFormSection:section];
268 [section addFormRow:row];
269 }
270
271 /* block external lan requests */
272 {
273 XLFormRowDescriptor *row = [XLFormRowDescriptor formRowDescriptorWithTag:HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS rowType:XLFormRowDescriptorTypeSelectorActionSheet title:@"Block external LAN requests"];
274 [self setYesNoSelectorOptionsForSetting:HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS host:host row:row withDefault:(![host isDefault])];
275
276 section = [XLFormSectionDescriptor formSection];
277 [section setTitle:@""];
278 [section setFooterTitle:[NSString stringWithFormat:@"Resources loaded from %@ will be blocked from loading page elements or making requests to LAN hosts (192.168.0.0/16, 172.16.0.0/12, etc.)", ([host isDefault] ? @"external hosts" : @"this host")]];
279 [form addFormSection:section];
280 [section addFormRow:row];
281 }
282 }
283
284 HostSettingsXLFormViewController *formController = [[HostSettingsXLFormViewController alloc] initWithForm:form];
285 [formController setDisappearCallback:^(HostSettingsXLFormViewController *form) {
286 if (![host isDefault])
287 [host setHostname:[[form formValues] objectForKey:HOST_SETTINGS_KEY_HOST]];
288
289 NSArray *keys = @[
290 HOST_SETTINGS_KEY_TLS,
291 HOST_SETTINGS_KEY_ALLOW_MIXED_MODE,
292 HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS,
293 HOST_SETTINGS_KEY_WHITELIST_COOKIES,
294 ];
295
296 for (NSString *key in keys) {
297 XLFormOptionsObject *opt = [[form formValues] objectForKey:key];
298 if (opt)
299 [host setSetting:key toValue:(NSString *)[opt valueData]];
300 }
301
302 [host save];
303 [HostSettings persist];
304 }];
305
306 [[self navigationController] pushViewController:formController animated:YES];
307 self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Hosts" style:UIBarButtonItemStylePlain target:nil action:nil];
308}
309
310- (void)setYesNoSelectorOptionsForSetting:(NSString *)key host:(HostSettings *)host row:(XLFormRowDescriptor *)row withDefault:(BOOL)withDefault
311{
312 NSMutableArray *opts = [[NSMutableArray alloc] init];
313 if (withDefault)
314 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_DEFAULT displayText:@"(Use Default)"]];
315 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_VALUE_YES displayText:@"Yes"]];
316 [opts addObject:[XLFormOptionsObject formOptionsObjectWithValue:HOST_SETTINGS_VALUE_NO displayText:@"No"]];
317 [row setSelectorOptions:opts];
318
319 NSString *val = [host setting:key];
320 if (val == nil)
321 val = HOST_SETTINGS_DEFAULT;
322
323 for (XLFormOptionsObject *opt in opts)
324 if ([[opt valueData] isEqualToString:val])
325 [row setValue:opt];
326}
327
328@end