diff --git a/Endless Tests/HSTSCache_Tests.m b/Endless Tests/HSTSCache_Tests.m
new file mode 100644
index 0000000..dfdb18d
--- /dev/null
+++ b/Endless Tests/HSTSCache_Tests.m
@@ -0,0 +1,104 @@
+#import
+#import
+#import
+
+#import "HSTSCache.h"
+
+#define TRACE_HSTS
+
+@interface HSTSCache_Tests : XCTestCase
+@end
+
+@implementation HSTSCache_Tests
+
+HSTSCache *hstsCache;
+
+- (void)setUp {
+ [super setUp];
+
+ hstsCache = [[HSTSCache alloc] init];
+}
+
+- (void)testParseHSTSHeader {
+ [hstsCache parseHSTSHeader:@"max-age=12345; includeSubDomains" forHost:@"example.com"];
+
+ NSDictionary *params = [hstsCache objectForKey:@"example.com"];
+ XCTAssertNotNil(params);
+ XCTAssertNotNil([params objectForKey:HSTS_KEY_ALLOW_SUBDOMAINS]);
+ XCTAssertNotNil([params objectForKey:HSTS_KEY_EXPIRATION]);
+
+ XCTAssertTrue([(NSDate *)[params objectForKey:HSTS_KEY_EXPIRATION] timeIntervalSince1970] - [[NSDate date] timeIntervalSince1970] >= 12340);
+}
+
+- (void)testIgnoreIPAddresses {
+ [hstsCache parseHSTSHeader:@"max-age=12345; includeSubDomains" forHost:@"127.0.0.1"];
+
+ NSDictionary *params = [hstsCache objectForKey:@"127.0.0.1"];
+ XCTAssertNil(params);
+}
+
+- (void)testParseUpdatedHSTSHeader {
+ [hstsCache parseHSTSHeader:@"max-age=12345; includeSubDomains" forHost:@"example.com"];
+
+ NSDictionary *params = [hstsCache objectForKey:@"example.com"];
+ XCTAssertNotNil(params);
+ XCTAssertNotNil([params objectForKey:HSTS_KEY_ALLOW_SUBDOMAINS]);
+
+ /* now a new request presents without includeSubDomains */
+ [hstsCache parseHSTSHeader:@"max-age=12345" forHost:@"example.com"];
+
+ params = [hstsCache objectForKey:@"example.com"];
+ XCTAssertNotNil(params);
+ XCTAssertNil([params objectForKey:HSTS_KEY_ALLOW_SUBDOMAINS]);
+}
+
+- (void)testParseEFFHSTSHeader {
+ /* weirdo header that eff sends (to cover old spec?) */
+ [hstsCache parseHSTSHeader:@"max-age=31536000; includeSubdomains, max-age=31536000; includeSubdomains" forHost:@"www.EFF.org"];
+
+ NSDictionary *params = [hstsCache objectForKey:@"www.eff.org"];
+ XCTAssertNotNil(params);
+ XCTAssertNotNil([params objectForKey:HSTS_KEY_ALLOW_SUBDOMAINS]);
+ XCTAssertNotNil([params objectForKey:HSTS_KEY_EXPIRATION]);
+}
+
+- (void)testURLRewriting {
+ [hstsCache parseHSTSHeader:@"max-age=31536000; includeSubdomains, max-age=31536000; includeSubdomains" forHost:@"www.EFF.org"];
+
+ NSURL *output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://www.eff.org/test"]];
+ XCTAssertTrue([[output absoluteString] isEqualToString:@"https://www.eff.org/test"]);
+
+ /* we didn't see the header for "eff.org", so subdomains have to be of www */
+ output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://subdomain.eff.org/test"]];
+ XCTAssertFalse([[output absoluteString] isEqualToString:@"https://subdomain.eff.org/test"]);
+
+ output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://subdomain.www.eff.org/test"]];
+ XCTAssertTrue([[output absoluteString] isEqualToString:@"https://subdomain.www.eff.org/test"]);
+
+ output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://www.eff.org:1234/?what#hi"]];
+ XCTAssertTrue([[output absoluteString] isEqualToString:@"https://www.eff.org:1234/?what#hi"]);
+
+ output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://www.eff.org:80/?what#hi"]];
+ XCTAssertTrue([[output absoluteString] isEqualToString:@"https://www.eff.org/?what#hi"]);
+}
+
+- (void)testExpiring {
+ [hstsCache parseHSTSHeader:@"max-age=2; includeSubDomains" forHost:@"example.com"];
+
+ NSURL *output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://www.example.com/"]];
+ XCTAssertTrue([[output absoluteString] isEqualToString:@"https://www.example.com/"]);
+
+ NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:4];
+
+ do {
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
+ if ([timeoutDate timeIntervalSinceNow] < 0)
+ break;
+ } while (TRUE);
+
+ /* expired */
+ output = [hstsCache rewrittenURI:[NSURL URLWithString:@"http://www.example.com/"]];
+ XCTAssertTrue([[output absoluteString] isEqualToString:@"http://www.example.com/"]);
+}
+
+@end
diff --git a/Endless.xcodeproj/project.pbxproj b/Endless.xcodeproj/project.pbxproj
index ba937fe..0f101f0 100644
--- a/Endless.xcodeproj/project.pbxproj
+++ b/Endless.xcodeproj/project.pbxproj
@@ -8,9 +8,10 @@
/* Begin PBXBuildFile section */
010EEA661A43A536001E8B65 /* CookieController.m in Sources */ = {isa = PBXBuildFile; fileRef = 010EEA651A43A536001E8B65 /* CookieController.m */; };
- 010EEA691A43C8CF001E8B65 /* CookieWhitelist.m in Sources */ = {isa = PBXBuildFile; fileRef = 010EEA681A43C8CF001E8B65 /* CookieWhitelist.m */; };
+ 010EEA691A43C8CF001E8B65 /* CookieJar.m in Sources */ = {isa = PBXBuildFile; fileRef = 010EEA681A43C8CF001E8B65 /* CookieJar.m */; };
0135F4761A3D2931005A8F16 /* SearchEngines.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0135F4751A3D2931005A8F16 /* SearchEngines.plist */; };
0135F47F1A3E548F005A8F16 /* WebViewTab.m in Sources */ = {isa = PBXBuildFile; fileRef = 0135F47E1A3E548F005A8F16 /* WebViewTab.m */; };
+ 016B2FCB1A53466D002D2730 /* hsts_preload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 016B2FCA1A53466D002D2730 /* hsts_preload.plist */; };
01801E981A32CA2A002B4718 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 01801E971A32CA2A002B4718 /* main.m */; };
01801E9B1A32CA2A002B4718 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 01801E9A1A32CA2A002B4718 /* AppDelegate.m */; };
01801EA11A32CA2A002B4718 /* WebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 01801EA01A32CA2A002B4718 /* WebViewController.m */; };
@@ -23,10 +24,13 @@
018333E91A35746500670CD1 /* https-everywhere_rules.plist in Resources */ = {isa = PBXBuildFile; fileRef = 018333E71A35746500670CD1 /* https-everywhere_rules.plist */; };
018333EA1A35746500670CD1 /* https-everywhere_targets.plist in Resources */ = {isa = PBXBuildFile; fileRef = 018333E81A35746500670CD1 /* https-everywhere_targets.plist */; };
01D741281A44DF1C007B7033 /* WebViewMenuController.m in Sources */ = {isa = PBXBuildFile; fileRef = 01D741271A44DF1C007B7033 /* WebViewMenuController.m */; };
- 01D7412A1A45EDD1007B7033 /* CookieWhitelist_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01D741291A45EDD1007B7033 /* CookieWhitelist_Tests.m */; };
+ 01D7412A1A45EDD1007B7033 /* CookieJar_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01D741291A45EDD1007B7033 /* CookieJar_Tests.m */; };
01D7412C1A45F8EB007B7033 /* injected.js in Resources */ = {isa = PBXBuildFile; fileRef = 01D7412B1A45F8EB007B7033 /* injected.js */; };
01D7412F1A466AF0007B7033 /* NSString+JavascriptEscape.m in Sources */ = {isa = PBXBuildFile; fileRef = 01D7412E1A466AF0007B7033 /* NSString+JavascriptEscape.m */; };
01D741321A49EA14007B7033 /* HTTPSEverywhereRuleController.m in Sources */ = {isa = PBXBuildFile; fileRef = 01D741311A49EA14007B7033 /* HTTPSEverywhereRuleController.m */; };
+ 01F7CB491A5253DD00F42B73 /* HSTSCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F7CB481A5253DD00F42B73 /* HSTSCache.m */; };
+ 01F7CB4B1A526B9C00F42B73 /* HSTSCache_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F7CB4A1A526B9C00F42B73 /* HSTSCache_Tests.m */; };
+ 01F7CB4E1A52FC4E00F42B73 /* NSString+IPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F7CB4D1A52FC4E00F42B73 /* NSString+IPAddress.m */; };
01F8793B1A4108DD00A63654 /* URLBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F8793A1A4108DD00A63654 /* URLBlocker.m */; };
01F879411A4112E500A63654 /* URLBlocker_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F879401A4112E500A63654 /* URLBlocker_Tests.m */; };
01F879441A41140D00A63654 /* https-everywhere_mock_rules.plist in Resources */ = {isa = PBXBuildFile; fileRef = 01F879421A41140D00A63654 /* https-everywhere_mock_rules.plist */; };
@@ -55,11 +59,12 @@
/* Begin PBXFileReference section */
010EEA641A43A536001E8B65 /* CookieController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CookieController.h; sourceTree = ""; };
010EEA651A43A536001E8B65 /* CookieController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CookieController.m; sourceTree = ""; };
- 010EEA671A43C8CF001E8B65 /* CookieWhitelist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CookieWhitelist.h; sourceTree = ""; };
- 010EEA681A43C8CF001E8B65 /* CookieWhitelist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CookieWhitelist.m; sourceTree = ""; };
+ 010EEA671A43C8CF001E8B65 /* CookieJar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CookieJar.h; sourceTree = ""; };
+ 010EEA681A43C8CF001E8B65 /* CookieJar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CookieJar.m; sourceTree = ""; };
0135F4751A3D2931005A8F16 /* SearchEngines.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = SearchEngines.plist; path = Endless/Resources/SearchEngines.plist; sourceTree = ""; };
0135F47D1A3E548F005A8F16 /* WebViewTab.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebViewTab.h; sourceTree = ""; };
0135F47E1A3E548F005A8F16 /* WebViewTab.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewTab.m; sourceTree = ""; };
+ 016B2FCA1A53466D002D2730 /* hsts_preload.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = hsts_preload.plist; path = Endless/Resources/hsts_preload.plist; sourceTree = ""; };
01801E921A32CA2A002B4718 /* Endless.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Endless.app; sourceTree = BUILT_PRODUCTS_DIR; };
01801E961A32CA2A002B4718 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
01801E971A32CA2A002B4718 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
@@ -84,12 +89,17 @@
018333EB1A357D8B00670CD1 /* libPods-OCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-OCMock.a"; path = "Pods/build/Debug-iphoneos/libPods-OCMock.a"; sourceTree = ""; };
01D741261A44DF1C007B7033 /* WebViewMenuController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebViewMenuController.h; sourceTree = ""; };
01D741271A44DF1C007B7033 /* WebViewMenuController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebViewMenuController.m; sourceTree = ""; };
- 01D741291A45EDD1007B7033 /* CookieWhitelist_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CookieWhitelist_Tests.m; sourceTree = ""; };
+ 01D741291A45EDD1007B7033 /* CookieJar_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CookieJar_Tests.m; sourceTree = ""; };
01D7412B1A45F8EB007B7033 /* injected.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = injected.js; path = Endless/Resources/injected.js; sourceTree = ""; };
01D7412D1A466AF0007B7033 /* NSString+JavascriptEscape.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+JavascriptEscape.h"; sourceTree = ""; };
01D7412E1A466AF0007B7033 /* NSString+JavascriptEscape.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+JavascriptEscape.m"; sourceTree = ""; };
01D741301A49EA14007B7033 /* HTTPSEverywhereRuleController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPSEverywhereRuleController.h; sourceTree = ""; };
01D741311A49EA14007B7033 /* HTTPSEverywhereRuleController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPSEverywhereRuleController.m; sourceTree = ""; };
+ 01F7CB471A5253DD00F42B73 /* HSTSCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSTSCache.h; sourceTree = ""; };
+ 01F7CB481A5253DD00F42B73 /* HSTSCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSTSCache.m; sourceTree = ""; };
+ 01F7CB4A1A526B9C00F42B73 /* HSTSCache_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSTSCache_Tests.m; sourceTree = ""; };
+ 01F7CB4C1A52FC4E00F42B73 /* NSString+IPAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+IPAddress.h"; sourceTree = ""; };
+ 01F7CB4D1A52FC4E00F42B73 /* NSString+IPAddress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+IPAddress.m"; sourceTree = ""; };
01F879391A4108DD00A63654 /* URLBlocker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = URLBlocker.h; sourceTree = ""; };
01F8793A1A4108DD00A63654 /* URLBlocker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLBlocker.m; sourceTree = ""; };
01F879401A4112E500A63654 /* URLBlocker_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = URLBlocker_Tests.m; sourceTree = ""; };
@@ -156,10 +166,12 @@
children = (
01801E991A32CA2A002B4718 /* AppDelegate.h */,
01801E9A1A32CA2A002B4718 /* AppDelegate.m */,
- 010EEA671A43C8CF001E8B65 /* CookieWhitelist.h */,
- 010EEA681A43C8CF001E8B65 /* CookieWhitelist.m */,
+ 010EEA671A43C8CF001E8B65 /* CookieJar.h */,
+ 010EEA681A43C8CF001E8B65 /* CookieJar.m */,
010EEA641A43A536001E8B65 /* CookieController.h */,
010EEA651A43A536001E8B65 /* CookieController.m */,
+ 01F7CB471A5253DD00F42B73 /* HSTSCache.h */,
+ 01F7CB481A5253DD00F42B73 /* HSTSCache.m */,
01D741301A49EA14007B7033 /* HTTPSEverywhereRuleController.h */,
01D741311A49EA14007B7033 /* HTTPSEverywhereRuleController.m */,
018333C81A3505FB00670CD1 /* HTTPSEverywhere.h */,
@@ -183,11 +195,13 @@
01801E951A32CA2A002B4718 /* Supporting Files */ = {
isa = PBXGroup;
children = (
- 01D7412D1A466AF0007B7033 /* NSString+JavascriptEscape.h */,
- 01D7412E1A466AF0007B7033 /* NSString+JavascriptEscape.m */,
+ 018333CF1A351B3B00670CD1 /* Endless-Prefix.pch */,
01801E961A32CA2A002B4718 /* Info.plist */,
01801E971A32CA2A002B4718 /* main.m */,
- 018333CF1A351B3B00670CD1 /* Endless-Prefix.pch */,
+ 01F7CB4C1A52FC4E00F42B73 /* NSString+IPAddress.h */,
+ 01F7CB4D1A52FC4E00F42B73 /* NSString+IPAddress.m */,
+ 01D7412D1A466AF0007B7033 /* NSString+JavascriptEscape.h */,
+ 01D7412E1A466AF0007B7033 /* NSString+JavascriptEscape.m */,
);
name = "Supporting Files";
path = Endless;
@@ -199,6 +213,7 @@
01801EC21A3360F8002B4718 /* InAppSettings.bundle */,
01F8794A1A41232E00A63654 /* credits.html */,
01D7412B1A45F8EB007B7033 /* injected.js */,
+ 016B2FCA1A53466D002D2730 /* hsts_preload.plist */,
018333E71A35746500670CD1 /* https-everywhere_rules.plist */,
018333E81A35746500670CD1 /* https-everywhere_targets.plist */,
0135F4751A3D2931005A8F16 /* SearchEngines.plist */,
@@ -211,9 +226,10 @@
018333D81A35727C00670CD1 /* Endless Tests */ = {
isa = PBXGroup;
children = (
+ 01D741291A45EDD1007B7033 /* CookieJar_Tests.m */,
+ 01F7CB4A1A526B9C00F42B73 /* HSTSCache_Tests.m */,
018333DB1A35727C00670CD1 /* HTTPSEverywhere_Tests.m */,
01F879401A4112E500A63654 /* URLBlocker_Tests.m */,
- 01D741291A45EDD1007B7033 /* CookieWhitelist_Tests.m */,
018333D91A35727C00670CD1 /* Supporting Files */,
);
path = "Endless Tests";
@@ -345,6 +361,7 @@
01F8794F1A412FA500A63654 /* urlblocker_targets.plist in Resources */,
01801EA61A32CA2A002B4718 /* Images.xcassets in Resources */,
01F8794C1A4124FE00A63654 /* credits.html in Resources */,
+ 016B2FCB1A53466D002D2730 /* hsts_preload.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -441,9 +458,11 @@
01F8793B1A4108DD00A63654 /* URLBlocker.m in Sources */,
01801EC01A335BEC002B4718 /* URLInterceptor.m in Sources */,
018333CA1A3505FB00670CD1 /* HTTPSEverywhere.m in Sources */,
+ 01F7CB491A5253DD00F42B73 /* HSTSCache.m in Sources */,
+ 01F7CB4E1A52FC4E00F42B73 /* NSString+IPAddress.m in Sources */,
0135F47F1A3E548F005A8F16 /* WebViewTab.m in Sources */,
010EEA661A43A536001E8B65 /* CookieController.m in Sources */,
- 010EEA691A43C8CF001E8B65 /* CookieWhitelist.m in Sources */,
+ 010EEA691A43C8CF001E8B65 /* CookieJar.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -451,8 +470,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 01F7CB4B1A526B9C00F42B73 /* HSTSCache_Tests.m in Sources */,
01F879411A4112E500A63654 /* URLBlocker_Tests.m in Sources */,
- 01D7412A1A45EDD1007B7033 /* CookieWhitelist_Tests.m in Sources */,
+ 01D7412A1A45EDD1007B7033 /* CookieJar_Tests.m in Sources */,
018333DC1A35727C00670CD1 /* HTTPSEverywhere_Tests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/Endless/AppDelegate.h b/Endless/AppDelegate.h
index be1dfe4..979cef0 100644
--- a/Endless/AppDelegate.h
+++ b/Endless/AppDelegate.h
@@ -2,6 +2,7 @@
#import
#import "CookieJar.h"
+#import "HSTSCache.h"
#import "WebViewController.h"
@interface AppDelegate : UIResponder
@@ -14,6 +15,7 @@
@property (strong, atomic) WebViewController *webViewController;
@property (strong, atomic) CookieJar *cookieJar;
+@property (strong, atomic) HSTSCache *hstsCache;
@property (readonly, strong, nonatomic) NSMutableDictionary *searchEngines;
diff --git a/Endless/Endless-Prefix.pch b/Endless/Endless-Prefix.pch
index 84cf2d2..6714eed 100644
--- a/Endless/Endless-Prefix.pch
+++ b/Endless/Endless-Prefix.pch
@@ -18,6 +18,9 @@
/* be verbose about javascript IPC */
//# define TRACE_IPC
+/* be verbose about HTTP Strict Transport Security */
+//# define TRACE_HSTS
+
#endif
#endif
diff --git a/Endless/HSTSCache.h b/Endless/HSTSCache.h
new file mode 100644
index 0000000..451fad1
--- /dev/null
+++ b/Endless/HSTSCache.h
@@ -0,0 +1,30 @@
+#import
+
+#define HSTS_HEADER @"Strict-Transport-Security"
+#define HSTS_KEY_EXPIRATION @"expiration"
+#define HSTS_KEY_ALLOW_SUBDOMAINS @"allowSubdomains"
+#define HSTS_KEY_PRELOADED @"preloaded"
+
+/* subclassing NSMutableDictionary is not easy, so we have to use composition */
+
+@interface HSTSCache : NSObject
+{
+ NSMutableDictionary *_dict;
+}
+
+@property NSMutableDictionary *dict;
+
++ (HSTSCache *)retrieve;
+
+- (void)persist;
+- (NSURL *)rewrittenURI:(NSURL *)URL;
+- (void)parseHSTSHeader:(NSString *)header forHost:(NSString *)host;
+
+/* NSMutableDictionary composition pass-throughs */
+- (id)objectForKey:(id)aKey;
+- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
+- (void)setValue:(id)value forKey:(NSString *)key;
+- (void)removeObjectForKey:(id)aKey;
+- (NSArray *)allKeys;
+
+@end
diff --git a/Endless/HSTSCache.m b/Endless/HSTSCache.m
new file mode 100644
index 0000000..9a988af
--- /dev/null
+++ b/Endless/HSTSCache.m
@@ -0,0 +1,220 @@
+#import "AppDelegate.h"
+#import "HSTSCache.h"
+#import "NSString+IPAddress.h"
+
+/* rfc6797 HTTP Strict Transport Security */
+
+/* note that UIWebView has its own HSTS cache that comes preloaded with a big plist of hosts, but we can't change it or manually add to it */
+
+@implementation HSTSCache
+
+static NSDictionary *_preloadedHosts;
+AppDelegate *appDelegate;
+
++ (NSString *)hstsCachePath
+{
+ NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
+ return [path stringByAppendingPathComponent:@"hsts_cache.plist"];
+}
+
+- (HSTSCache *)init
+{
+ self = [super init];
+
+ _dict = [[NSMutableDictionary alloc] init];
+ appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
+
+ return self;
+}
+
++ (HSTSCache *)retrieve
+{
+ HSTSCache *hc = [[HSTSCache alloc] init];
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ if ([fileManager fileExistsAtPath:[[self class] hstsCachePath]]) {
+ hc.dict = [NSMutableDictionary dictionaryWithContentsOfFile:[[self class] hstsCachePath]];
+ }
+ else {
+ hc.dict = [[NSMutableDictionary alloc] initWithCapacity:50];
+ }
+
+ /* mix in preloaded */
+ NSString *path = [[NSBundle mainBundle] pathForResource:@"hsts_preload" ofType:@"plist"];
+ if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
+ NSDictionary *tmp = [NSDictionary dictionaryWithContentsOfFile:path];
+ for (NSString *host in [tmp allKeys]) {
+ NSDictionary *hostdef = [tmp objectForKey:host];
+ NSMutableDictionary *v = [[NSMutableDictionary alloc] init];
+
+ [v setObject:[NSDate dateWithTimeIntervalSinceNow:(60 * 60 * 24 * 365)] forKey:HSTS_KEY_EXPIRATION];
+ [v setObject:@YES forKey:HSTS_KEY_PRELOADED];
+
+ NSNumber *is = [hostdef objectForKey:@"include_subdomains"];
+ if ([is intValue] == 1) {
+ [v setObject:@YES forKey:HSTS_KEY_ALLOW_SUBDOMAINS];
+ }
+
+ [[hc dict] setObject:v forKey:host];
+ }
+
+#ifdef TRACE_HSTS
+ NSLog(@"[HSTSCache] locked and loaded with %lu preloaded hosts", [tmp count]);
+#endif
+ }
+ else {
+ NSLog(@"[HSTSCache] no preload plist at %@", path);
+ }
+
+ return hc;
+}
+
+- (void)persist
+{
+ [self writeToFile:[[self class] hstsCachePath] atomically:YES];
+}
+
+- (NSURL *)rewrittenURI:(NSURL *)URL
+{
+ if (![[URL scheme] isEqualToString:@"http"]) {
+ return URL;
+ }
+
+ NSString *host = [[URL host] lowercaseString];
+ NSString *matchHost = [host copy];
+
+ /* 8.3: ignore when host is a bare ip address */
+ if ([host isValidIPAddress]) {
+ return URL;
+ }
+
+ NSDictionary *params = [self objectForKey:host];
+ if (params == nil) {
+ /* for a host of x.y.z.example.com, try y.z.example.com, z.example.com, example.com, etc. */
+ NSArray *hostp = [host componentsSeparatedByString:@"."];
+ for (int i = 1; i < [hostp count]; i++) {
+ NSString *wc = [[hostp subarrayWithRange:NSMakeRange(i, [hostp count] - i)] componentsJoinedByString:@"."];
+
+ if (((params = [self objectForKey:wc]) != nil) && [params objectForKey:HSTS_KEY_ALLOW_SUBDOMAINS]) {
+ matchHost = wc;
+ break;
+ }
+ }
+ }
+
+ if (params != nil) {
+ NSDate *exp = [params objectForKey:HSTS_KEY_EXPIRATION];
+ if ([exp timeIntervalSince1970] < [[NSDate date] timeIntervalSince1970]) {
+#ifdef TRACE_HSTS
+ NSLog(@"[HSTSCache] entry for %@ expired at %@", matchHost, exp);
+#endif
+ [self removeObjectForKey:matchHost];
+ params = nil;
+ }
+ }
+
+ if (params == nil) {
+ return URL;
+ }
+
+ NSURLComponents *URLc = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO];
+
+ [URLc setScheme:@"https"];
+
+ /* 8.3.5: nullify port unless it's a non-standard one */
+ if ([URLc port] != nil && [[URLc port] intValue] == 80) {
+ [URLc setPort:nil];
+ }
+
+#ifdef TRACE_HSTS
+ NSLog(@"[HSTSCache] %@rewrote %@ to %@", ([params objectForKey:HSTS_KEY_PRELOADED] ? @"[preloaded] " : @""), URL, [URLc URL]);
+#endif
+
+ return [URLc URL];
+}
+
+- (void)parseHSTSHeader:(NSString *)header forHost:(NSString *)host
+{
+ NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithCapacity:3];
+ host = [host lowercaseString];
+
+ /* 8.1.1: reject caching when host is a bare ip address */
+ if ([host isValidIPAddress])
+ return;
+
+#ifdef TRACE_HSTS
+ NSLog(@"[HSTSCache] [%@] %@", host, header);
+#endif
+
+ NSArray *kvs = [header componentsSeparatedByString:@";"];
+ for (NSString *kv in kvs) {
+ NSArray *kvparts = [kv componentsSeparatedByString:@"="];
+ NSString *key, *value;
+
+ key = [kvparts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+
+ if ([kvparts count] > 1) {
+ value = [[kvparts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] stringByReplacingOccurrencesOfString:@"\"" withString:@""];
+ }
+
+ if ([[key lowercaseString] isEqualToString:@"max-age"]) {
+ long age = [value longLongValue];
+
+ if (age == 0) {
+#ifdef TRACE_HSTS
+ NSLog(@"[HSTSCache] [%@] got max-age=0, deleting", host);
+#endif
+ /* TODO: if a preloaded entry exists, cache a negative entry */
+ [self removeObjectForKey:host];
+ return;
+ }
+ else {
+ NSDate *expire = [[NSDate date] dateByAddingTimeInterval:age];
+ [params setObject:expire forKey:HSTS_KEY_EXPIRATION];
+ }
+ }
+ else if ([[key lowercaseString] isEqualToString:@"includesubdomains"]) {
+ [params setObject:@YES forKey:HSTS_KEY_ALLOW_SUBDOMAINS];
+ }
+ else if ([[key lowercaseString] isEqualToString:@"preload"]) {
+ /* ignore */
+ }
+ else {
+#ifdef TRACE_HSTS
+ NSLog(@"[HSTSCache] [%@] unknown parameter %@", host, key);
+#endif
+ }
+ }
+
+ if ([params objectForKey:HSTS_KEY_EXPIRATION]) {
+ [self setValue:params forKey:host];
+ }
+}
+
+/* NSMutableDictionary composition pass-throughs */
+
+- (id)objectForKey:(id)aKey
+{
+ return [[self dict] objectForKey:aKey];
+}
+
+- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile
+{
+ return [[self dict] writeToFile:path atomically:useAuxiliaryFile];
+}
+
+- (void)setValue:(id)value forKey:(NSString *)key
+{
+ [[self dict] setValue:value forKey:key];
+}
+
+- (void)removeObjectForKey:(id)aKey
+{
+ [[self dict] removeObjectForKey:aKey];
+}
+
+- (NSArray *)allKeys
+{
+ return [[self dict] allKeys];
+}
+
+@end
diff --git a/Endless/HTTPSEverywhere.m b/Endless/HTTPSEverywhere.m
index 2ff3aa1..200b300 100644
--- a/Endless/HTTPSEverywhere.m
+++ b/Endless/HTTPSEverywhere.m
@@ -20,9 +20,8 @@ + (NSString *)disabledRulesPath
+ (NSDictionary *)rules
{
if (_rules == nil) {
- NSFileManager *fm = [NSFileManager defaultManager];
NSString *path = [[NSBundle mainBundle] pathForResource:@"https-everywhere_rules" ofType:@"plist"];
- if (![fm fileExistsAtPath:path]) {
+ if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
NSLog(@"[HTTPSEverywhere] no rule plist at %@", path);
abort();
}
@@ -40,9 +39,8 @@ + (NSDictionary *)rules
+ (NSMutableDictionary *)disabledRules
{
if (_disabledRules == nil) {
- NSFileManager *fm = [NSFileManager defaultManager];
NSString *path = [[self class] disabledRulesPath];
- if ([fm fileExistsAtPath:path]) {
+ if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
_disabledRules = [NSMutableDictionary dictionaryWithContentsOfFile:path];
#ifdef TRACE_HTTPS_EVERYWHERE
diff --git a/Endless/NSString+IPAddress.h b/Endless/NSString+IPAddress.h
new file mode 100644
index 0000000..c016daa
--- /dev/null
+++ b/Endless/NSString+IPAddress.h
@@ -0,0 +1,7 @@
+#import
+
+@interface NSString (IPAddress)
+
+- (BOOL)isValidIPAddress;
+
+@end
diff --git a/Endless/NSString+IPAddress.m b/Endless/NSString+IPAddress.m
new file mode 100644
index 0000000..87ed277
--- /dev/null
+++ b/Endless/NSString+IPAddress.m
@@ -0,0 +1,22 @@
+#import
+
+#include
+
+@implementation NSString (IPAddress)
+
+- (BOOL)isValidIPAddress
+{
+ struct in_addr dst;
+ int success;
+ const char *utf8 = [self UTF8String];
+
+ success = inet_pton(AF_INET, utf8, &dst);
+ if (success != 1) {
+ struct in6_addr dst6;
+ success = inet_pton(AF_INET6, utf8, &dst6);
+ }
+
+ return (success == 1);
+}
+
+@end
diff --git a/Endless/Resources/credits.html b/Endless/Resources/credits.html
index 4a12a5b..4acdef6 100644
--- a/Endless/Resources/credits.html
+++ b/Endless/Resources/credits.html
@@ -177,5 +177,22 @@
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+
+
+
+Chromium Authors (HSTS Preload List)
+
+Copyright 2014 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+* Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+