From 2ad242f926e6e53b685162cfa06dd99d1d729b97 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Thu, 28 May 2020 23:44:27 -0700 Subject: [PATCH 01/72] checkpoint --- DaemonMain.m | 22 +++ SCDaemon.h | 18 ++ SCDaemon.m | 18 ++ SelfControl-Bridging-Header.h | 4 + SelfControl.xcodeproj/project.pbxproj | 168 +++++++++++++++++- .../xcschemes/selfcontrold.xcscheme | 78 ++++++++ selfcontrold-Info.plist | 67 +++++++ 7 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 DaemonMain.m create mode 100644 SCDaemon.h create mode 100644 SCDaemon.m create mode 100644 SelfControl-Bridging-Header.h create mode 100644 SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme create mode 100755 selfcontrold-Info.plist diff --git a/DaemonMain.m b/DaemonMain.m new file mode 100644 index 00000000..2d2a97da --- /dev/null +++ b/DaemonMain.m @@ -0,0 +1,22 @@ +// +// DaemonMain.m +// SelfControl +// +// Created by Charlie Stigler on 5/28/20. +// + +#import +#import "SCDaemon.h" + +int main(int argc, const char *argv[]) { + // get the daemon object going + SCDaemon* daemon = [[SCDaemon alloc] init]; + [daemon start]; + + NSLog(@"running forever"); + + // never gonna give you up, never gonna let you down, never gonna run around and desert you... + [[NSRunLoop currentRunLoop] run]; + + return 0; +} diff --git a/SCDaemon.h b/SCDaemon.h new file mode 100644 index 00000000..fc015dcb --- /dev/null +++ b/SCDaemon.h @@ -0,0 +1,18 @@ +// +// SCDaemon.h +// SelfControl +// +// Created by Charlie Stigler on 5/28/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SCDaemon : NSObject + +- (void)start; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SCDaemon.m b/SCDaemon.m new file mode 100644 index 00000000..66f93d40 --- /dev/null +++ b/SCDaemon.m @@ -0,0 +1,18 @@ +// +// SCDaemon.m +// SelfControl +// +// Created by Charlie Stigler on 5/28/20. +// + +#import "SCDaemon.h" + +@implementation SCDaemon + +- (void)start { + NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block:^(NSTimer * _Nonnull timer) { + NSLog(@"still running still running"); + }]; +} + +@end diff --git a/SelfControl-Bridging-Header.h b/SelfControl-Bridging-Header.h new file mode 100644 index 00000000..1b2cb5d6 --- /dev/null +++ b/SelfControl-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 5fea1715..aef60952 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -41,6 +41,12 @@ CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB6840A5247A3E5500E51564 /* SelfControlKillerIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */; }; CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73615F19E4FDA000E0924F /* AllowlistScraper.m */; }; + CB74D1152480E506002B2079 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB32D2AD21902D9D00B8CD68 /* IOKit.framework */; }; + CB74D1162480E506002B2079 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E901D0F397FFA006DE6E4 /* Security.framework */; }; + CB74D1182480E506002B2079 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97325FDCFA39411CA2CEA /* Foundation.framework */; }; + CB74D1192480E506002B2079 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */ = {isa = PBXBuildFile; fileRef = CB74D1032480E4D9002B2079 /* DaemonMain.m */; }; + CB74D1202480E566002B2079 /* SCDaemon.m in Sources */ = {isa = PBXBuildFile; fileRef = CB74D0FD2480E3E6002B2079 /* SCDaemon.m */; }; CB90BF830F49F430006D202D /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; CB9365620F8581B000EF284E /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CB9365610F8581B000EF284E /* dsa_pub.pem */; }; CB9366E80F85BEF100EF284E /* NSRemoveTemplate.jpg in Resources */ = {isa = PBXBuildFile; fileRef = CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */; }; @@ -205,6 +211,12 @@ CB7026191CD7177400D7C7F0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; CB73615E19E4FDA000E0924F /* AllowlistScraper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AllowlistScraper.h; sourceTree = ""; }; CB73615F19E4FDA000E0924F /* AllowlistScraper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AllowlistScraper.m; sourceTree = ""; }; + CB74D0F62480E2F8002B2079 /* SelfControl-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SelfControl-Bridging-Header.h"; sourceTree = ""; }; + CB74D0FC2480E3E6002B2079 /* SCDaemon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemon.h; sourceTree = ""; }; + CB74D0FD2480E3E6002B2079 /* SCDaemon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemon.m; sourceTree = ""; }; + CB74D1032480E4D9002B2079 /* DaemonMain.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DaemonMain.m; sourceTree = ""; }; + CB74D11D2480E506002B2079 /* selfcontrold */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = selfcontrold; sourceTree = BUILT_PRODUCTS_DIR; }; + CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "selfcontrold-Info.plist"; path = "/Users/charlie/dev/selfcontrol/selfcontrold-Info.plist"; sourceTree = ""; }; CB90BF810F49F430006D202D /* HostImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostImporter.h; sourceTree = ""; }; CB90BF820F49F430006D202D /* HostImporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HostImporter.m; sourceTree = ""; }; CB9365610F8581B000EF284E /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; @@ -374,6 +386,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CB74D1142480E506002B2079 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CB74D1152480E506002B2079 /* IOKit.framework in Frameworks */, + CB74D1162480E506002B2079 /* Security.framework in Frameworks */, + CB74D1182480E506002B2079 /* Foundation.framework in Frameworks */, + CB74D1192480E506002B2079 /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CB9C80F419CFB79700CDCAE1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -427,6 +450,7 @@ CB9C80F719CFB79700CDCAE1 /* SelfControl Killer.app */, CB9C811B19CFBA8500CDCAE1 /* SCKillerHelper */, CB0EEF5D20FD8CE00024D27B /* SelfControlTests.xctest */, + CB74D11D2480E506002B2079 /* selfcontrold */, ); name = Products; sourceTree = ""; @@ -466,6 +490,7 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( + CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */, CBBF4E8C1582F8E000E364D9 /* Localizable.strings */, CB40A8670FBC7DE700167727 /* SelfControlBlocklist.icns */, CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */, @@ -603,10 +628,14 @@ CBD266C611ED82DB00042CD8 /* Helper Tools */ = { isa = PBXGroup; children = ( + CB74D1032480E4D9002B2079 /* DaemonMain.m */, CBA2AFD70F39EC46005AFEBE /* HelperMain.h */, CBA2AFD80F39EC46005AFEBE /* HelperMain.m */, + CB74D0FC2480E3E6002B2079 /* SCDaemon.h */, + CB74D0FD2480E3E6002B2079 /* SCDaemon.m */, CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */, CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */, + CB74D0F62480E2F8002B2079 /* SelfControl-Bridging-Header.h */, ); name = "Helper Tools"; sourceTree = ""; @@ -686,6 +715,23 @@ productReference = CB0EEF5D20FD8CE00024D27B /* SelfControlTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + CB74D1052480E506002B2079 /* selfcontrold */ = { + isa = PBXNativeTarget; + buildConfigurationList = CB74D11A2480E506002B2079 /* Build configuration list for PBXNativeTarget "selfcontrold" */; + buildPhases = ( + CB74D1062480E506002B2079 /* ShellScript */, + CB74D1072480E506002B2079 /* Sources */, + CB74D1142480E506002B2079 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = selfcontrold; + productName = "selfcontrol-helper"; + productReference = CB74D11D2480E506002B2079 /* selfcontrold */; + productType = "com.apple.product-type.tool"; + }; CB9C80F619CFB79700CDCAE1 /* SelfControl Killer */ = { isa = PBXNativeTarget; buildConfigurationList = CB9C811119CFB79700CDCAE1 /* Build configuration list for PBXNativeTarget "SelfControl Killer" */; @@ -749,6 +795,7 @@ TargetAttributes = { 8D1107260486CEB800E47090 = { DevelopmentTeam = L6W5L88KN7; + LastSwiftMigration = 1150; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { @@ -762,6 +809,9 @@ ProvisioningStyle = Automatic; TestTargetID = 8D1107260486CEB800E47090; }; + CB74D1052480E506002B2079 = { + DevelopmentTeam = L6W5L88KN7; + }; CB9C80F619CFB79700CDCAE1 = { CreatedOnToolsVersion = 6.0.1; DevelopmentTeam = L6W5L88KN7; @@ -772,6 +822,9 @@ DevelopmentTeam = L6W5L88KN7; ProvisioningStyle = Manual; }; + CB9F02EE16C10CDE003054EE = { + DevelopmentTeam = L6W5L88KN7; + }; CBA2AFD10F39EC12005AFEBE = { DevelopmentTeam = L6W5L88KN7; ProvisioningStyle = Manual; @@ -809,6 +862,7 @@ CB9C80F619CFB79700CDCAE1 /* SelfControl Killer */, CB9C811A19CFBA8500CDCAE1 /* SCKillerHelper */, CB0EEF5C20FD8CE00024D27B /* SelfControlTests */, + CB74D1052480E506002B2079 /* selfcontrold */, ); }; /* End PBXProject section */ @@ -925,7 +979,25 @@ shellPath = /bin/sh; shellScript = "LOCATION=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\n\n# Usually set by Xcode\nCODE_SIGN_IDENTITY=\"Developer ID Application: Charlie Stigler (L6W5L88KN7)\"\n\necho \"location: \"\necho $LOCATION\n\n# codesign --verbose --force --sign \"$CODE_SIGN_IDENTITY\" \"$LOCATION/Sparkle.framework/Versions/A\"\n"; }; - CBFF218A3108765AAB5DE542 /* [CP] Copy Pods Resources */ = { + CB74D1062480E506002B2079 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "buildNumber=$(xcodebuild -project SelfControl.xcodeproj/ -showBuildSettings | grep \"CURRENT_PROJECT_VERSION\" | sed 's/[ ]*CURRENT_PROJECT_VERSION = //')\n\necho \"#define SELFCONTROL_VERSION_STRING @\\\"${buildNumber}\\\"\" > \"${PROJECT_DIR}/version-header.h\"\n"; + }; + + CBFF218A3108765AAB5DE542 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1058,6 +1130,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CB74D1072480E506002B2079 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */, + CB74D1202480E566002B2079 /* SCDaemon.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CB9C80F319CFB79700CDCAE1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1120,6 +1201,11 @@ target = CB9C811A19CFBA8500CDCAE1 /* SCKillerHelper */; targetProxy = CB9C812D19CFBBA300CDCAE1 /* PBXContainerItemProxy */; }; + CB9F02F316C10CE6003054EE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8D1107260486CEB800E47090 /* SelfControl */; + targetProxy = CB9F02F216C10CE6003054EE /* PBXContainerItemProxy */; + }; CBE2BB5519D10CBB0077124F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CB9C811A19CFBA8500CDCAE1 /* SCKillerHelper */; @@ -1296,6 +1382,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = DBA01017E61D6B843CA3758D /* Pods-SelfControl.debug.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "Developer ID Application"; COMBINE_HIDPI_IMAGES = YES; @@ -1321,6 +1408,9 @@ PRODUCT_NAME = SelfControl; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; + SWIFT_OBJC_BRIDGING_HEADER = "SelfControl-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1328,6 +1418,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5F489F28028DC38C046B36D3 /* Pods-SelfControl.release.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 3.0.3; @@ -1350,6 +1441,8 @@ PRODUCT_NAME = SelfControl; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; + SWIFT_OBJC_BRIDGING_HEADER = "SelfControl-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1521,6 +1614,70 @@ }; name = Release; }; + CB74D11B2480E506002B2079 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = L6W5L88KN7; + GCC_DYNAMIC_NO_PIC = NO; + GCC_MODEL_TUNING = G5; + GCC_NO_COMMON_BLOCKS = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h"; + GCC_VERSION = ""; + INFOPLIST_FILE = "org.eyebeam.SelfControl copy-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + OTHER_LDFLAGS = ( + "-framework", + Foundation, + "-framework", + AppKit, + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "org.eyebeam.SelfControl-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + CB74D11C2480E506002B2079 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = L6W5L88KN7; + GCC_MODEL_TUNING = G5; + GCC_NO_COMMON_BLOCKS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SYSTEM_LIBRARY_DIR)/Frameworks/AppKit.framework/Headers/AppKit.h"; + GCC_VERSION = ""; + INFOPLIST_FILE = "org.eyebeam.SelfControl copy-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + OTHER_LDFLAGS = ( + "-framework", + Foundation, + "-framework", + AppKit, + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "org.eyebeam.SelfControl-Bridging-Header.h"; + ZERO_LINK = NO; + }; + name = Release; + }; CB9C811219CFB79700CDCAE1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1775,6 +1932,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CB74D11A2480E506002B2079 /* Build configuration list for PBXNativeTarget "selfcontrold" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CB74D11B2480E506002B2079 /* Debug */, + CB74D11C2480E506002B2079 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; CB9C811119CFB79700CDCAE1 /* Build configuration list for PBXNativeTarget "SelfControl Killer" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme b/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme new file mode 100644 index 00000000..a1e1d4b3 --- /dev/null +++ b/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/selfcontrold-Info.plist b/selfcontrold-Info.plist new file mode 100755 index 00000000..bcdada61 --- /dev/null +++ b/selfcontrold-Info.plist @@ -0,0 +1,67 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + SelfControl + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + selfcontrol + + CFBundleTypeIconFile + SelfControlBlocklist.icns + CFBundleTypeName + SelfControl Blocklist + CFBundleTypeRole + Editor + LSTypeIsPackage + + NSPersistentStoreTypeKey + Binary + + + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + SelfControlIcon + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSApplicationCategoryType + public.app-category.productivity + LSHasLocalizedDisplayName + + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSAppleScriptEnabled + + NSHumanReadableCopyright + Free and open-source under the GPL. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + SUEnableAutomaticChecks + + SUEnableJavaScript + + SUFeedURL + https://selfcontrolapp.com/SelfControlAppcast.xml + SUPublicDSAKeyFile + dsa_pub.pem + + From abe889cfd408b6946b807b6241e7836bae1270e7 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 30 May 2020 23:30:37 -0700 Subject: [PATCH 02/72] checkpoint --- AppController.m | 23 ++++--------- SCDaemon.h => Daemon/SCDaemon.h | 0 Daemon/SCDaemon.m | 48 +++++++++++++++++++++++++++ Daemon/SCDaemonProtocol.h | 22 ++++++++++++ Daemon/SCDaemonXPC.h | 17 ++++++++++ Daemon/SCDaemonXPC.m | 28 ++++++++++++++++ Daemon/org.eyebeam.selfcontrold.plist | 18 ++++++++++ SCDaemon.m | 18 ---------- SelfControl-Bridging-Header.h | 4 --- SelfControl.xcodeproj/project.pbxproj | 26 ++++++++++++--- 10 files changed, 161 insertions(+), 43 deletions(-) rename SCDaemon.h => Daemon/SCDaemon.h (100%) create mode 100644 Daemon/SCDaemon.m create mode 100644 Daemon/SCDaemonProtocol.h create mode 100644 Daemon/SCDaemonXPC.h create mode 100644 Daemon/SCDaemonXPC.m create mode 100644 Daemon/org.eyebeam.selfcontrold.plist delete mode 100644 SCDaemon.m delete mode 100644 SelfControl-Bridging-Header.h diff --git a/AppController.m b/AppController.m index d7626831..8698fc75 100755 --- a/AppController.m +++ b/AppController.m @@ -29,6 +29,7 @@ #import #import #import "SCSettings.h" +#import NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; @@ -637,7 +638,7 @@ - (void)installBlock { char* helperToolPath = [self selfControlHelperToolPathUTF8String]; NSUInteger helperToolPathSize = strlen(helperToolPath); AuthorizationItem right = { - kAuthorizationRightExecute, + kSMRightBlessPrivilegedHelper, helperToolPathSize, helperToolPath, 0 @@ -670,22 +671,12 @@ - (void)installBlock { // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid [settings_ synchronizeSettings]; - // We need to pass our UID to the helper tool. It needs to know whose defaults - // it should reading in order to properly load the blocklist. - char uidString[32]; - snprintf(uidString, sizeof(uidString), "%d", getuid()); - - FILE* commPipe; - - char* args[] = { uidString, "--install", NULL }; - status = AuthorizationExecuteWithPrivileges(authorizationRef, - helperToolPath, - kAuthorizationFlagDefaults, - args, - &commPipe); + CFErrorRef cfError; + BOOL result = (BOOL)SMJobBless(kSMDomainSystemLaunchd, (CFStringRef)@"org.eyebeams.selfcontrold", authorizationRef, NULL); + - if(status) { - NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %d", (int)status); + if(!result) { + NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %ds", (int)status); // reset settings on failure, and record that on disk ASAP [SCUtilities removeBlockFromSettings: settings_]; diff --git a/SCDaemon.h b/Daemon/SCDaemon.h similarity index 100% rename from SCDaemon.h rename to Daemon/SCDaemon.h diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m new file mode 100644 index 00000000..aa47b27b --- /dev/null +++ b/Daemon/SCDaemon.m @@ -0,0 +1,48 @@ +// +// SCDaemon.m +// SelfControl +// +// Created by Charlie Stigler on 5/28/20. +// + +#import "SCDaemon.h" +#import "SCDaemonProtocol.h" +#import "SCDaemonXPC.h" + +static NSString* serviceName = @"org.eyebeam.selfcontrold"; + +@interface SCDaemon () + +@property (nonatomic, strong, readwrite) NSXPCListener* listener; + +@end + +@implementation SCDaemon + +- (id) init { + _listener = [[NSXPCListener alloc] initWithMachServiceName: serviceName]; + _listener.delegate = self; + + return self; +} + +- (void)start { + [self.listener resume]; + NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block:^(NSTimer * _Nonnull timer) { + NSLog(@"still running still running"); + }]; +} + +#pragma mark - NSXPCListenerDelegate + +- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { + SCDaemonXPC* scdXPC = [[SCDaemonXPC alloc] init]; + newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol: @protocol(SCDaemonProtocol)]; + newConnection.exportedObject = scdXPC; + + [newConnection resume]; + + return YES; +} + +@end diff --git a/Daemon/SCDaemonProtocol.h b/Daemon/SCDaemonProtocol.h new file mode 100644 index 00000000..fbb42409 --- /dev/null +++ b/Daemon/SCDaemonProtocol.h @@ -0,0 +1,22 @@ +// +// SCDaemonProtocol.h +// selfcontrold +// +// Created by Charlie Stigler on 5/30/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol SCDaemonProtocol + +- (BOOL) install; + +- (BOOL) checkup; + +- (BOOL) getVersion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonXPC.h b/Daemon/SCDaemonXPC.h new file mode 100644 index 00000000..428e1785 --- /dev/null +++ b/Daemon/SCDaemonXPC.h @@ -0,0 +1,17 @@ +// +// SCDaemonXPC.h +// selfcontrold +// +// Created by Charlie Stigler on 5/30/20. +// + +#import +#import "SCDaemonProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SCDaemonXPC : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m new file mode 100644 index 00000000..751554b5 --- /dev/null +++ b/Daemon/SCDaemonXPC.m @@ -0,0 +1,28 @@ +// +// SCDaemonXPC.m +// selfcontrold +// +// Created by Charlie Stigler on 5/30/20. +// + +#import "SCDaemonXPC.h" + +@implementation SCDaemonXPC + +- (BOOL) install { + NSLog(@"XPC method called: install"); + return YES; +} + +- (BOOL) checkup { + NSLog(@"XPC method called: checkup"); + return YES; +} + +- (BOOL) getVersion { + NSLog(@"XPC method called: getVersion"); + return YES; +} + + +@end diff --git a/Daemon/org.eyebeam.selfcontrold.plist b/Daemon/org.eyebeam.selfcontrold.plist new file mode 100644 index 00000000..3513d0ce --- /dev/null +++ b/Daemon/org.eyebeam.selfcontrold.plist @@ -0,0 +1,18 @@ + + + + + Label + org.eyebeam.selfcontrold + RunAtLoad + + KeepAlive + + Nice + 5 + ProgramArguments + + /Library/PrivilegedHelperTools/org.eyebeam.selfcontrold + + + diff --git a/SCDaemon.m b/SCDaemon.m deleted file mode 100644 index 66f93d40..00000000 --- a/SCDaemon.m +++ /dev/null @@ -1,18 +0,0 @@ -// -// SCDaemon.m -// SelfControl -// -// Created by Charlie Stigler on 5/28/20. -// - -#import "SCDaemon.h" - -@implementation SCDaemon - -- (void)start { - NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block:^(NSTimer * _Nonnull timer) { - NSLog(@"still running still running"); - }]; -} - -@end diff --git a/SelfControl-Bridging-Header.h b/SelfControl-Bridging-Header.h deleted file mode 100644 index 1b2cb5d6..00000000 --- a/SelfControl-Bridging-Header.h +++ /dev/null @@ -1,4 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// - diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index aef60952..6b030bf7 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ CB74D1192480E506002B2079 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */ = {isa = PBXBuildFile; fileRef = CB74D1032480E4D9002B2079 /* DaemonMain.m */; }; CB74D1202480E566002B2079 /* SCDaemon.m in Sources */ = {isa = PBXBuildFile; fileRef = CB74D0FD2480E3E6002B2079 /* SCDaemon.m */; }; + CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = CB8086D324837607004B88BD /* SCDaemonXPC.m */; }; CB90BF830F49F430006D202D /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; CB9365620F8581B000EF284E /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CB9365610F8581B000EF284E /* dsa_pub.pem */; }; CB9366E80F85BEF100EF284E /* NSRemoveTemplate.jpg in Resources */ = {isa = PBXBuildFile; fileRef = CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */; }; @@ -211,12 +212,15 @@ CB7026191CD7177400D7C7F0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; CB73615E19E4FDA000E0924F /* AllowlistScraper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AllowlistScraper.h; sourceTree = ""; }; CB73615F19E4FDA000E0924F /* AllowlistScraper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AllowlistScraper.m; sourceTree = ""; }; - CB74D0F62480E2F8002B2079 /* SelfControl-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SelfControl-Bridging-Header.h"; sourceTree = ""; }; CB74D0FC2480E3E6002B2079 /* SCDaemon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemon.h; sourceTree = ""; }; CB74D0FD2480E3E6002B2079 /* SCDaemon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemon.m; sourceTree = ""; }; CB74D1032480E4D9002B2079 /* DaemonMain.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DaemonMain.m; sourceTree = ""; }; CB74D11D2480E506002B2079 /* selfcontrold */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = selfcontrold; sourceTree = BUILT_PRODUCTS_DIR; }; CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "selfcontrold-Info.plist"; path = "/Users/charlie/dev/selfcontrol/selfcontrold-Info.plist"; sourceTree = ""; }; + CB74D122248374E6002B2079 /* SCDaemonProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonProtocol.h; sourceTree = ""; }; + CB8086D224837607004B88BD /* SCDaemonXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonXPC.h; sourceTree = ""; }; + CB8086D324837607004B88BD /* SCDaemonXPC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonXPC.m; sourceTree = ""; }; + CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = org.eyebeam.selfcontrold.plist; sourceTree = ""; }; CB90BF810F49F430006D202D /* HostImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostImporter.h; sourceTree = ""; }; CB90BF820F49F430006D202D /* HostImporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HostImporter.m; sourceTree = ""; }; CB9365610F8581B000EF284E /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; @@ -490,7 +494,6 @@ 29B97317FDCFA39411CA2CEA /* Resources */ = { isa = PBXGroup; children = ( - CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */, CBBF4E8C1582F8E000E364D9 /* Localizable.strings */, CB40A8670FBC7DE700167727 /* SelfControlBlocklist.icns */, CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */, @@ -584,6 +587,20 @@ name = Interfaces; sourceTree = ""; }; + CB74D121248371E7002B2079 /* Daemon */ = { + isa = PBXGroup; + children = ( + CB74D0FC2480E3E6002B2079 /* SCDaemon.h */, + CB74D0FD2480E3E6002B2079 /* SCDaemon.m */, + CB8086D224837607004B88BD /* SCDaemonXPC.h */, + CB8086D324837607004B88BD /* SCDaemonXPC.m */, + CB74D122248374E6002B2079 /* SCDaemonProtocol.h */, + CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */, + CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */, + ); + path = Daemon; + sourceTree = ""; + }; CB9C80F819CFB79700CDCAE1 /* SelfControl Killer */ = { isa = PBXGroup; children = ( @@ -631,11 +648,9 @@ CB74D1032480E4D9002B2079 /* DaemonMain.m */, CBA2AFD70F39EC46005AFEBE /* HelperMain.h */, CBA2AFD80F39EC46005AFEBE /* HelperMain.m */, - CB74D0FC2480E3E6002B2079 /* SCDaemon.h */, - CB74D0FD2480E3E6002B2079 /* SCDaemon.m */, + CB74D121248371E7002B2079 /* Daemon */, CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */, CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */, - CB74D0F62480E2F8002B2079 /* SelfControl-Bridging-Header.h */, ); name = "Helper Tools"; sourceTree = ""; @@ -1136,6 +1151,7 @@ files = ( CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */, CB74D1202480E566002B2079 /* SCDaemon.m in Sources */, + CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 52ef540f90008851d28a2daf9162802fd1f93a82 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 21 Jun 2020 22:53:53 -0700 Subject: [PATCH 03/72] Checkpoint - i h8 code signing --- AppController.m | 89 ++++++++++--------- Daemon/org.eyebeam.selfcontrold.plist | 21 ++--- Info.plist | 5 ++ SelfControl.xcodeproj/project.pbxproj | 58 ++++++++---- .../xcschemes/selfcontrold.xcscheme | 12 +-- selfcontrold-Info.plist | 4 + 6 files changed, 113 insertions(+), 76 deletions(-) diff --git a/AppController.m b/AppController.m index 8698fc75..58bbce22 100755 --- a/AppController.m +++ b/AppController.m @@ -75,8 +75,8 @@ - (NSString*)selfControlHelperToolPath { // Cache the path so it doesn't have to be searched for again. if(!path) { NSBundle* thisBundle = [NSBundle mainBundle]; - path = [thisBundle pathForAuxiliaryExecutable: @"org.eyebeam.SelfControl"]; - } + path = [thisBundle.bundlePath stringByAppendingString: @"/Contents/Library/LaunchServices/org.eyebeam.selfcontrold"]; + } return path; } @@ -635,6 +635,7 @@ - (void)installBlock { self.addingBlock = true; [self refreshUserInterface]; AuthorizationRef authorizationRef; + NSLog(@"helper tool path is %@", [self selfControlHelperToolPath]); char* helperToolPath = [self selfControlHelperToolPathUTF8String]; NSUInteger helperToolPathSize = strlen(helperToolPath); AuthorizationItem right = { @@ -672,11 +673,17 @@ - (void)installBlock { [settings_ synchronizeSettings]; CFErrorRef cfError; - BOOL result = (BOOL)SMJobBless(kSMDomainSystemLaunchd, (CFStringRef)@"org.eyebeams.selfcontrold", authorizationRef, NULL); + BOOL result = (BOOL)SMJobBless( + kSMDomainSystemLaunchd, + CFSTR("org.eyebeam.selfcontrold"), + authorizationRef, + &cfError); if(!result) { - NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %ds", (int)status); + NSError* error = CFBridgingRelease(cfError); + + NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %d and error %@", (int)status, error); // reset settings on failure, and record that on disk ASAP [SCUtilities removeBlockFromSettings: settings_]; @@ -696,43 +703,43 @@ - (void)installBlock { return; } - NSFileHandle* helperToolHandle = [[NSFileHandle alloc] initWithFileDescriptor: fileno(commPipe) closeOnDealloc: YES]; - - NSData* inData = [helperToolHandle readDataToEndOfFile]; - - - NSString* inDataString = [[NSString alloc] initWithData: inData encoding: NSUTF8StringEncoding]; - - if([inDataString isEqualToString: @""]) { - // reset settings on failure, and record that on disk ASAP - [SCUtilities removeBlockFromSettings: settings_]; - [settings_ synchronizeSettings]; - - NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain - code: -104 - userInfo: @{NSLocalizedDescriptionKey: @"Error -104: The helper tool crashed. This may cause unexpected errors."}]; - - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: err - waitUntilDone: YES]; - } - - int exitCode = [inDataString intValue]; - - if(exitCode) { - // reset settings on failure, and record that on disk ASAP - [SCUtilities removeBlockFromSettings: settings_]; - [settings_ synchronizeSettings]; - - NSError* err = [self errorFromHelperToolStatusCode: exitCode]; - - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: err - waitUntilDone: YES]; - } - - self.addingBlock = false; - [self refreshUserInterface]; +// NSFileHandle* helperToolHandle = [[NSFileHandle alloc] initWithFileDescriptor: fileno(commPipe) closeOnDealloc: YES]; +// +// NSData* inData = [helperToolHandle readDataToEndOfFile]; +// +// +// NSString* inDataString = [[NSString alloc] initWithData: inData encoding: NSUTF8StringEncoding]; +// +// if([inDataString isEqualToString: @""]) { +// // reset settings on failure, and record that on disk ASAP +// [SCUtilities removeBlockFromSettings: settings_]; +// [settings_ synchronizeSettings]; +// +// NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain +// code: -104 +// userInfo: @{NSLocalizedDescriptionKey: @"Error -104: The helper tool crashed. This may cause unexpected errors."}]; +// +// [NSApp performSelectorOnMainThread: @selector(presentError:) +// withObject: err +// waitUntilDone: YES]; +// } +// +// int exitCode = [inDataString intValue]; +// +// if(exitCode) { +// // reset settings on failure, and record that on disk ASAP +// [SCUtilities removeBlockFromSettings: settings_]; +// [settings_ synchronizeSettings]; +// +// NSError* err = [self errorFromHelperToolStatusCode: exitCode]; +// +// [NSApp performSelectorOnMainThread: @selector(presentError:) +// withObject: err +// waitUntilDone: YES]; +// } +// +// self.addingBlock = false; +// [self refreshUserInterface]; } } diff --git a/Daemon/org.eyebeam.selfcontrold.plist b/Daemon/org.eyebeam.selfcontrold.plist index 3513d0ce..91dc9b7a 100644 --- a/Daemon/org.eyebeam.selfcontrold.plist +++ b/Daemon/org.eyebeam.selfcontrold.plist @@ -4,15 +4,16 @@ Label org.eyebeam.selfcontrold - RunAtLoad - - KeepAlive - - Nice - 5 - ProgramArguments - - /Library/PrivilegedHelperTools/org.eyebeam.selfcontrold - + RunAtLoad + + MachServices + + org.eyebeam.selfcontrold + + + KeepAlive + + Nice + 5 diff --git a/Info.plist b/Info.plist index bcdada61..d824c1bd 100755 --- a/Info.plist +++ b/Info.plist @@ -63,5 +63,10 @@ https://selfcontrolapp.com/SelfControlAppcast.xml SUPublicDSAKeyFile dsa_pub.pem + SMPrivilegedExecutables + + org.eyebeam.selfcontrold + identifier "org.eyebeam.selfcontrold" and (cert leaf[subject.CN] = "Developer ID Application: Charlie Stigler (L6W5L88KN7)" or cert leaf[subject.CN] = "Apple Development: Charles Stigler (22JB4UZFSZ)") + diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 6b030bf7..9abac7b5 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ CB114284222CD4F0004B7868 /* SCSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBF3B573217BADD7006D5F52 /* SCSettings.m */; }; CB1B44BE23B7F1ED00EBA087 /* cheater-background.png in Resources */ = {isa = PBXBuildFile; fileRef = CB1B44BD23B7F1ED00EBA087 /* cheater-background.png */; }; CB1E7A6E0F9AEA9B00D158BC /* ERRORS in Resources */ = {isa = PBXBuildFile; fileRef = CB1E7A6D0F9AEA9B00D158BC /* ERRORS */; }; - CB1ED4480F56686400EFECEE /* org.eyebeam.SelfControl in Copy Helper Tools */ = {isa = PBXBuildFile; fileRef = CBA2AFD20F39EC12005AFEBE /* org.eyebeam.SelfControl */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CB1ED4480F56686400EFECEE /* org.eyebeam.SelfControl in Copy Executable Helper Tools */ = {isa = PBXBuildFile; fileRef = CBA2AFD20F39EC12005AFEBE /* org.eyebeam.SelfControl */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CB20C5D8245699D700B9D749 /* version-header.h in Sources */ = {isa = PBXBuildFile; fileRef = CB20C5D7245699D700B9D749 /* version-header.h */; }; CB249FED19D782230087BBB6 /* SelfControlIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = CB249FEC19D782230087BBB6 /* SelfControlIcon.icns */; }; CB25806216C1FDBE0059C99A /* BlockManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806116C1FDBE0059C99A /* BlockManager.m */; }; @@ -92,7 +92,9 @@ CBD4848F19D768C90020F949 /* PreferencesAdvancedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD4848D19D768C90020F949 /* PreferencesAdvancedViewController.m */; }; CBDB118C2084239D0010397E /* FirstTime.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBDB118E2084239D0010397E /* FirstTime.xib */; }; CBDF919A225C5A9700358B95 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; - CBE2BB5619D10CCC0077124F /* SCKillerHelper in Copy Helper Tools */ = {isa = PBXBuildFile; fileRef = CB9C811B19CFBA8500CDCAE1 /* SCKillerHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CBDFFF4724A0450200622CEE /* org.eyebeam.selfcontrold in Copy Daemon Launch Service */ = {isa = PBXBuildFile; fileRef = CB74D11D2480E506002B2079 /* org.eyebeam.selfcontrold */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CBDFFF4924A0711800622CEE /* org.eyebeam.selfcontrold.plist in Resources */ = {isa = PBXBuildFile; fileRef = CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */; }; + CBE2BB5619D10CCC0077124F /* SCKillerHelper in Copy Executable Helper Tools */ = {isa = PBXBuildFile; fileRef = CB9C811B19CFBA8500CDCAE1 /* SCKillerHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CBE4401B0F4BE0670062A1FE /* ThunderbirdPreferenceParser.m in Sources */ = {isa = PBXBuildFile; fileRef = CBE4401A0F4BE0670062A1FE /* ThunderbirdPreferenceParser.m */; }; CBE44FEB19E50900004E9706 /* AllowlistScraper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73615F19E4FDA000E0924F /* AllowlistScraper.m */; }; CBE5C40B0F4D4531003DB900 /* ButtonWithPopupMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = CBE5C40A0F4D4531003DB900 /* ButtonWithPopupMenu.m */; }; @@ -133,16 +135,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - CB2359D90F4541AB0030F59C /* Copy Helper Tools */ = { + CB2359D90F4541AB0030F59C /* Copy Executable Helper Tools */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 6; files = ( - CBE2BB5619D10CCC0077124F /* SCKillerHelper in Copy Helper Tools */, - CB1ED4480F56686400EFECEE /* org.eyebeam.SelfControl in Copy Helper Tools */, + CBE2BB5619D10CCC0077124F /* SCKillerHelper in Copy Executable Helper Tools */, + CB1ED4480F56686400EFECEE /* org.eyebeam.SelfControl in Copy Executable Helper Tools */, ); - name = "Copy Helper Tools"; + name = "Copy Executable Helper Tools"; runOnlyForDeploymentPostprocessing = 0; }; CB9C811919CFBA8500CDCAE1 /* CopyFiles */ = { @@ -165,6 +167,17 @@ name = "Copy Helper Tools"; runOnlyForDeploymentPostprocessing = 0; }; + CBDFFF4624A044C900622CEE /* Copy Daemon Launch Service */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Contents/Library/LaunchServices; + dstSubfolderSpec = 1; + files = ( + CBDFFF4724A0450200622CEE /* org.eyebeam.selfcontrold in Copy Daemon Launch Service */, + ); + name = "Copy Daemon Launch Service"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -215,7 +228,7 @@ CB74D0FC2480E3E6002B2079 /* SCDaemon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemon.h; sourceTree = ""; }; CB74D0FD2480E3E6002B2079 /* SCDaemon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemon.m; sourceTree = ""; }; CB74D1032480E4D9002B2079 /* DaemonMain.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DaemonMain.m; sourceTree = ""; }; - CB74D11D2480E506002B2079 /* selfcontrold */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = selfcontrold; sourceTree = BUILT_PRODUCTS_DIR; }; + CB74D11D2480E506002B2079 /* org.eyebeam.selfcontrold */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = org.eyebeam.selfcontrold; sourceTree = BUILT_PRODUCTS_DIR; }; CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "selfcontrold-Info.plist"; path = "/Users/charlie/dev/selfcontrol/selfcontrold-Info.plist"; sourceTree = ""; }; CB74D122248374E6002B2079 /* SCDaemonProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonProtocol.h; sourceTree = ""; }; CB8086D224837607004B88BD /* SCDaemonXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonXPC.h; sourceTree = ""; }; @@ -265,7 +278,6 @@ CBC2F8650F4674E300CF2A42 /* LaunchctlHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LaunchctlHelper.h; sourceTree = ""; }; CBCA91101960D87300AFD20C /* PacketFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PacketFilter.h; sourceTree = ""; }; CBCA91111960D87300AFD20C /* PacketFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PacketFilter.m; sourceTree = ""; }; - CBCA9116196123E600AFD20C /* SelfControl.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = SelfControl.entitlements; sourceTree = ""; }; CBCA91271961381F00AFD20C /* tr */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CBCA912B1961384600AFD20C /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HelperCommon.h; sourceTree = ""; }; @@ -345,6 +357,7 @@ CBDB11942084801B0010397E /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/PreferencesAdvancedViewController.strings; sourceTree = ""; }; CBDB11962084801B0010397E /* da */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; CBDB11972084801C0010397E /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/InfoPlist.strings; sourceTree = ""; }; + CBDFFF4B24A07DB300622CEE /* SelfControl.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SelfControl.entitlements; sourceTree = ""; }; CBE440190F4BE0670062A1FE /* ThunderbirdPreferenceParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThunderbirdPreferenceParser.h; sourceTree = ""; }; CBE4401A0F4BE0670062A1FE /* ThunderbirdPreferenceParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThunderbirdPreferenceParser.m; sourceTree = ""; }; CBE5C4090F4D4531003DB900 /* ButtonWithPopupMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ButtonWithPopupMenu.h; sourceTree = ""; }; @@ -454,7 +467,7 @@ CB9C80F719CFB79700CDCAE1 /* SelfControl Killer.app */, CB9C811B19CFBA8500CDCAE1 /* SCKillerHelper */, CB0EEF5D20FD8CE00024D27B /* SelfControlTests.xctest */, - CB74D11D2480E506002B2079 /* selfcontrold */, + CB74D11D2480E506002B2079 /* org.eyebeam.selfcontrold */, ); name = Products; sourceTree = ""; @@ -462,10 +475,10 @@ 29B97314FDCFA39411CA2CEA /* SelfControl */ = { isa = PBXGroup; children = ( + CBDFFF4B24A07DB300622CEE /* SelfControl.entitlements */, CB20C5D7245699D700B9D749 /* version-header.h */, CBD4848519D7611F0020F949 /* Podfile */, CBD4848619D7611F0020F949 /* TemplateIcon2x.png */, - CBCA9116196123E600AFD20C /* SelfControl.entitlements */, CB1E7A6D0F9AEA9B00D158BC /* ERRORS */, CB4294DF0F53D865008E10CA /* Classes */, CB0EEF5E20FD8CE00024D27B /* SelfControlTests */, @@ -694,7 +707,8 @@ 8D1107290486CEB800E47090 /* Resources */, 8D11072C0486CEB800E47090 /* Sources */, 8D11072E0486CEB800E47090 /* Frameworks */, - CB2359D90F4541AB0030F59C /* Copy Helper Tools */, + CB2359D90F4541AB0030F59C /* Copy Executable Helper Tools */, + CBDFFF4624A044C900622CEE /* Copy Daemon Launch Service */, CB5E5FF81C3A5FD10038F331 /* ShellScript */, 7D55904F6A3D627B837007A9 /* [CP] Embed Pods Frameworks */, CBFF218A3108765AAB5DE542 /* [CP] Copy Pods Resources */, @@ -730,9 +744,9 @@ productReference = CB0EEF5D20FD8CE00024D27B /* SelfControlTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - CB74D1052480E506002B2079 /* selfcontrold */ = { + CB74D1052480E506002B2079 /* org.eyebeam.selfcontrold */ = { isa = PBXNativeTarget; - buildConfigurationList = CB74D11A2480E506002B2079 /* Build configuration list for PBXNativeTarget "selfcontrold" */; + buildConfigurationList = CB74D11A2480E506002B2079 /* Build configuration list for PBXNativeTarget "org.eyebeam.selfcontrold" */; buildPhases = ( CB74D1062480E506002B2079 /* ShellScript */, CB74D1072480E506002B2079 /* Sources */, @@ -742,9 +756,9 @@ ); dependencies = ( ); - name = selfcontrold; + name = org.eyebeam.selfcontrold; productName = "selfcontrol-helper"; - productReference = CB74D11D2480E506002B2079 /* selfcontrold */; + productReference = CB74D11D2480E506002B2079 /* org.eyebeam.selfcontrold */; productType = "com.apple.product-type.tool"; }; CB9C80F619CFB79700CDCAE1 /* SelfControl Killer */ = { @@ -826,6 +840,7 @@ }; CB74D1052480E506002B2079 = { DevelopmentTeam = L6W5L88KN7; + ProvisioningStyle = Manual; }; CB9C80F619CFB79700CDCAE1 = { CreatedOnToolsVersion = 6.0.1; @@ -877,7 +892,7 @@ CB9C80F619CFB79700CDCAE1 /* SelfControl Killer */, CB9C811A19CFBA8500CDCAE1 /* SCKillerHelper */, CB0EEF5C20FD8CE00024D27B /* SelfControlTests */, - CB74D1052480E506002B2079 /* selfcontrold */, + CB74D1052480E506002B2079 /* org.eyebeam.selfcontrold */, ); }; /* End PBXProject section */ @@ -894,6 +909,7 @@ CB9366E90F85BEF100EF284E /* NSAddTemplate.jpg in Resources */, CBDB118C2084239D0010397E /* FirstTime.xib in Resources */, CBBF4EE515830D7300E364D9 /* TimerWindow.xib in Resources */, + CBDFFF4924A0711800622CEE /* org.eyebeam.selfcontrold.plist in Resources */, CB0385E219D77051004614B6 /* PreferencesGeneralViewController.xib in Resources */, CB1E7A6E0F9AEA9B00D158BC /* ERRORS in Resources */, CB40A8680FBC7DE700167727 /* SelfControlBlocklist.icns in Resources */, @@ -1484,7 +1500,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_ENTITLEMENTS = SelfControl.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -1533,7 +1548,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_ENTITLEMENTS = SelfControl.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -1635,6 +1649,8 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = L6W5L88KN7; @@ -1653,6 +1669,7 @@ "-framework", AppKit, ); + PRODUCT_BUNDLE_IDENTIFIER = org.eyebeam.selfcontrold; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1668,6 +1685,8 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = L6W5L88KN7; @@ -1684,6 +1703,7 @@ "-framework", AppKit, ); + PRODUCT_BUNDLE_IDENTIFIER = org.eyebeam.selfcontrold; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1948,7 +1968,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - CB74D11A2480E506002B2079 /* Build configuration list for PBXNativeTarget "selfcontrold" */ = { + CB74D11A2480E506002B2079 /* Build configuration list for PBXNativeTarget "org.eyebeam.selfcontrold" */ = { isa = XCConfigurationList; buildConfigurations = ( CB74D11B2480E506002B2079 /* Debug */, diff --git a/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme b/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme index a1e1d4b3..ed26c720 100644 --- a/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme +++ b/SelfControl.xcodeproj/xcshareddata/xcschemes/selfcontrold.xcscheme @@ -15,8 +15,8 @@ @@ -45,8 +45,8 @@ @@ -62,8 +62,8 @@ diff --git a/selfcontrold-Info.plist b/selfcontrold-Info.plist index bcdada61..05487db8 100755 --- a/selfcontrold-Info.plist +++ b/selfcontrold-Info.plist @@ -63,5 +63,9 @@ https://selfcontrolapp.com/SelfControlAppcast.xml SUPublicDSAKeyFile dsa_pub.pem + SMAuthorizedClients + + identifier "org.eyebeam.selfcontrol" and (cert leaf[subject.CN] = "Developer ID Application: Charlie Stigler (L6W5L88KN7)" or cert leaf[subject.CN] = "Apple Development: Charles Stigler (22JB4UZFSZ)") + From 5dea3cf978c42477f34072cb207a43972cc5405e Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 4 Jul 2020 13:34:33 -0700 Subject: [PATCH 04/72] code signing kinda works --- .../selfcontrold-Info.plist | 10 +++--- Info.plist | 10 +++--- SelfControl.xcodeproj/project.pbxproj | 32 ++++++++++++------- 3 files changed, 31 insertions(+), 21 deletions(-) rename selfcontrold-Info.plist => Daemon/selfcontrold-Info.plist (84%) diff --git a/selfcontrold-Info.plist b/Daemon/selfcontrold-Info.plist similarity index 84% rename from selfcontrold-Info.plist rename to Daemon/selfcontrold-Info.plist index 05487db8..c5d33627 100755 --- a/selfcontrold-Info.plist +++ b/Daemon/selfcontrold-Info.plist @@ -30,7 +30,7 @@ CFBundleIconFile SelfControlIcon CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) + org.eyebeam.selfcontrold CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -55,6 +55,10 @@ MainMenu NSPrincipalClass NSApplication + SMAuthorizedClients + + anchor apple generic and identifier "org.eyebeam.SelfControl" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) + SUEnableAutomaticChecks SUEnableJavaScript @@ -63,9 +67,5 @@ https://selfcontrolapp.com/SelfControlAppcast.xml SUPublicDSAKeyFile dsa_pub.pem - SMAuthorizedClients - - identifier "org.eyebeam.selfcontrol" and (cert leaf[subject.CN] = "Developer ID Application: Charlie Stigler (L6W5L88KN7)" or cert leaf[subject.CN] = "Apple Development: Charles Stigler (22JB4UZFSZ)") - diff --git a/Info.plist b/Info.plist index d824c1bd..9ffa70bf 100755 --- a/Info.plist +++ b/Info.plist @@ -55,6 +55,11 @@ MainMenu NSPrincipalClass NSApplication + SMPrivilegedExecutables + + org.eyebeam.selfcontrold + anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) + SUEnableAutomaticChecks SUEnableJavaScript @@ -63,10 +68,5 @@ https://selfcontrolapp.com/SelfControlAppcast.xml SUPublicDSAKeyFile dsa_pub.pem - SMPrivilegedExecutables - - org.eyebeam.selfcontrold - identifier "org.eyebeam.selfcontrold" and (cert leaf[subject.CN] = "Developer ID Application: Charlie Stigler (L6W5L88KN7)" or cert leaf[subject.CN] = "Apple Development: Charles Stigler (22JB4UZFSZ)") - diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 9abac7b5..ab5158d6 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ CB5DFCBA2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB5DFCBB2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; + CB62FC3024B11A4F00ADBC40 /* selfcontrold-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */; }; CB6840A5247A3E5500E51564 /* SelfControlKillerIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */; }; CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73615F19E4FDA000E0924F /* AllowlistScraper.m */; }; CB74D1152480E506002B2079 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB32D2AD21902D9D00B8CD68 /* IOKit.framework */; }; @@ -219,6 +220,7 @@ CB587E4F0F50FE8800C66A09 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = /System/Library/Frameworks/SystemConfiguration.framework; sourceTree = ""; }; CB5DFCB52251DD1F0084CEC2 /* SCConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCConstants.h; sourceTree = ""; }; CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCConstants.m; sourceTree = ""; }; + CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "selfcontrold-Info.plist"; sourceTree = ""; }; CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = SelfControlKillerIcon.icns; sourceTree = ""; }; CB6ED5662085C70200012817 /* da */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = da; path = da.lproj/FirstTime.xib; sourceTree = ""; }; CB7026181CD7177400D7C7F0 /* fr */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -229,7 +231,6 @@ CB74D0FD2480E3E6002B2079 /* SCDaemon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemon.m; sourceTree = ""; }; CB74D1032480E4D9002B2079 /* DaemonMain.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DaemonMain.m; sourceTree = ""; }; CB74D11D2480E506002B2079 /* org.eyebeam.selfcontrold */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = org.eyebeam.selfcontrold; sourceTree = BUILT_PRODUCTS_DIR; }; - CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "selfcontrold-Info.plist"; path = "/Users/charlie/dev/selfcontrol/selfcontrold-Info.plist"; sourceTree = ""; }; CB74D122248374E6002B2079 /* SCDaemonProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonProtocol.h; sourceTree = ""; }; CB8086D224837607004B88BD /* SCDaemonXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonXPC.h; sourceTree = ""; }; CB8086D324837607004B88BD /* SCDaemonXPC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonXPC.m; sourceTree = ""; }; @@ -480,6 +481,7 @@ CBD4848519D7611F0020F949 /* Podfile */, CBD4848619D7611F0020F949 /* TemplateIcon2x.png */, CB1E7A6D0F9AEA9B00D158BC /* ERRORS */, + CB74D121248371E7002B2079 /* Daemon */, CB4294DF0F53D865008E10CA /* Classes */, CB0EEF5E20FD8CE00024D27B /* SelfControlTests */, 29B97323FDCFA39411CA2CEA /* Frameworks */, @@ -608,7 +610,7 @@ CB8086D224837607004B88BD /* SCDaemonXPC.h */, CB8086D324837607004B88BD /* SCDaemonXPC.m */, CB74D122248374E6002B2079 /* SCDaemonProtocol.h */, - CB74D11E2480E506002B2079 /* selfcontrold-Info.plist */, + CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */, CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */, ); path = Daemon; @@ -661,7 +663,6 @@ CB74D1032480E4D9002B2079 /* DaemonMain.m */, CBA2AFD70F39EC46005AFEBE /* HelperMain.h */, CBA2AFD80F39EC46005AFEBE /* HelperMain.m */, - CB74D121248371E7002B2079 /* Daemon */, CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */, CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */, ); @@ -908,6 +909,7 @@ CB9366E80F85BEF100EF284E /* NSRemoveTemplate.jpg in Resources */, CB9366E90F85BEF100EF284E /* NSAddTemplate.jpg in Resources */, CBDB118C2084239D0010397E /* FirstTime.xib in Resources */, + CB62FC3024B11A4F00ADBC40 /* selfcontrold-Info.plist in Resources */, CBBF4EE515830D7300E364D9 /* TimerWindow.xib in Resources */, CBDFFF4924A0711800622CEE /* org.eyebeam.selfcontrold.plist in Resources */, CB0385E219D77051004614B6 /* PreferencesGeneralViewController.xib in Resources */, @@ -1664,10 +1666,14 @@ INFOPLIST_FILE = "org.eyebeam.SelfControl copy-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; OTHER_LDFLAGS = ( - "-framework", - Foundation, - "-framework", - AppKit, + "-sectcreate", + __TEXT, + __info_plist, + "Daemon/selfcontrold-Info.plist", + "-sectcreate", + __TEXT, + __launchd_plist, + Daemon/org.eyebeam.selfcontrold.plist, ); PRODUCT_BUNDLE_IDENTIFIER = org.eyebeam.selfcontrold; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1698,10 +1704,14 @@ INFOPLIST_FILE = "org.eyebeam.SelfControl copy-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; OTHER_LDFLAGS = ( - "-framework", - Foundation, - "-framework", - AppKit, + "-sectcreate", + __TEXT, + __info_plist, + "Daemon/selfcontrold-Info.plist", + "-sectcreate", + __TEXT, + __launchd_plist, + Daemon/org.eyebeam.selfcontrold.plist, ); PRODUCT_BUNDLE_IDENTIFIER = org.eyebeam.selfcontrold; PRODUCT_NAME = "$(TARGET_NAME)"; From 884c7eca4f10d280d8e9d53f5f74d0627f5fbf59 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Fri, 10 Jul 2020 21:45:53 -0700 Subject: [PATCH 05/72] checkpoint stupid threads bah --- AppController.m | 34 +++++++ DaemonMain.m => Daemon/DaemonMain.m | 0 Daemon/SCDaemon.m | 6 +- Daemon/SCDaemonBlockMethods.h | 18 ++++ Daemon/SCDaemonBlockMethods.m | 62 +++++++++++++ Daemon/SCDaemonProtocol.h | 4 +- Daemon/SCDaemonXPC.m | 24 +++-- SCAppXPC.h | 23 +++++ SCAppXPC.m | 127 ++++++++++++++++++++++++++ SelfControl.xcodeproj/project.pbxproj | 36 +++++++- 10 files changed, 322 insertions(+), 12 deletions(-) rename DaemonMain.m => Daemon/DaemonMain.m (100%) create mode 100644 Daemon/SCDaemonBlockMethods.h create mode 100644 Daemon/SCDaemonBlockMethods.m create mode 100644 SCAppXPC.h create mode 100644 SCAppXPC.m diff --git a/AppController.m b/AppController.m index 58bbce22..1243aa28 100755 --- a/AppController.m +++ b/AppController.m @@ -30,9 +30,16 @@ #import #import "SCSettings.h" #import +#import "SCAppXPC.h" NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; +@interface AppController () {} + +@property (atomic, strong, readwrite) SCAppXPC* xpc; + +@end + @implementation AppController { NSWindowController* getStartedWindowController; } @@ -341,6 +348,10 @@ - (void)applicationWillFinishLaunching:(NSNotification *)notification { - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [NSApplication sharedApplication].delegate = self; + + // start up our daemon XPC + self.xpc = [SCAppXPC new]; + [self.xpc connectToHelperTool]; // Register observers on both distributed and normal notification centers // to receive notifications from the helper tool and the other parts of the @@ -397,6 +408,15 @@ - (BOOL)blockIsRunning { } - (IBAction)showDomainList:(id)sender { + [self.xpc getVersion]; + [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID + blocklist: [settings_ valueForKey: @"Blocklist"] + endDate: [settings_ valueForKey: @"BlockEndDate"] + authorization: nil + reply:^(NSError * _Nonnull error) { + NSLog(@"WOO started block with error %@", error); + }]; + BOOL addBlockIsOngoing = self.addingBlock; if([self blockIsRunning] || addBlockIsOngoing) { NSAlert* blockInProgressAlert = [[NSAlert alloc] init]; @@ -703,6 +723,20 @@ - (void)installBlock { return; } + // ok, the new helper tool is installed! refresh the connection, then it's time to start the block + dispatch_async(dispatch_get_main_queue(), ^{ + [self.xpc refreshConnection]; + NSLog(@"Refreshed connection!"); +// [self.xpc getVersion]; + [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID + blocklist: [settings_ valueForKey: @"Blocklist"] + endDate: [settings_ valueForKey: @"BlockEndDate"] + authorization: nil + reply:^(NSError * _Nonnull error) { + NSLog(@"WOO started block with error %@", error); + }]; + }); + // NSFileHandle* helperToolHandle = [[NSFileHandle alloc] initWithFileDescriptor: fileno(commPipe) closeOnDealloc: YES]; // // NSData* inData = [helperToolHandle readDataToEndOfFile]; diff --git a/DaemonMain.m b/Daemon/DaemonMain.m similarity index 100% rename from DaemonMain.m rename to Daemon/DaemonMain.m diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index aa47b27b..5d6bd002 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -28,8 +28,8 @@ - (id) init { - (void)start { [self.listener resume]; - NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: YES block:^(NSTimer * _Nonnull timer) { - NSLog(@"still running still running"); + NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 15 repeats: YES block:^(NSTimer * _Nonnull timer) { + NSLog(@"still running"); }]; } @@ -42,6 +42,8 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne [newConnection resume]; + NSLog(@"Accepted new connection!"); + return YES; } diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h new file mode 100644 index 00000000..55df7ae6 --- /dev/null +++ b/Daemon/SCDaemonBlockMethods.h @@ -0,0 +1,18 @@ +// +// SCDaemonBlockMethods.h +// org.eyebeam.selfcontrold +// +// Created by Charlie Stigler on 7/4/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SCDaemonBlockMethods : NSObject + ++ (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m new file mode 100644 index 00000000..c35584dc --- /dev/null +++ b/Daemon/SCDaemonBlockMethods.m @@ -0,0 +1,62 @@ +// +// SCDaemonBlockMethods.m +// org.eyebeam.selfcontrold +// +// Created by Charlie Stigler on 7/4/20. +// + +#import "SCDaemonBlockMethods.h" +#import "SCSettings.h" +#import "HelperCommon.h" + +NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; + +@implementation SCDaemonBlockMethods + ++ (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"startign block in methods"); + if (blockIsRunningInSettingsOrDefaults(controllingUID)) { + NSLog(@"ERROR: Block is already running"); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -222 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Block is already running", nil) + }]; + reply(err); + return; + } + + // clear any legacy block information - no longer useful since we're using SCSettings now + // (and could potentially confuse things) + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + [settings clearLegacySettings]; + + // update SCSettings with the blocklist and end date that've been requested + NSLog(@"Replacing settings end date %@ with %@, and blocklist %@ with %@", [settings valueForKey: @"BlockEndDate"], endDate, [settings valueForKey: @"Blocklist"], blocklist); + [settings setValue: blocklist forKey: @"Blocklist"]; + [settings setValue: endDate forKey: @"BlockEndDate"]; + + if([blocklist count] <= 0 || ![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { + NSLog(@"ERROR: Blocklist is empty, or block end date is in the past"); + NSLog(@"Block End Date: %@", [settings valueForKey: @"BlockEndDate"]); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -210 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Blocklist is empty, or block end date is in the past", nil) + }]; + reply(err); + return; + } + + addRulesToFirewall(controllingUID); + [settings setValue: @YES forKey: @"BlockIsRunning"]; + [settings synchronizeSettings]; // synchronize ASAP since BlockIsRunning is a really important one + + // TODO: is this still necessary in the new daemon world? + sendConfigurationChangedNotification(); + + // Clear all caches if the user has the correct preference set, so + // that blocked pages are not loaded from a cache. + clearCachesIfRequested(controllingUID); + + NSLog(@"INFO: Block successfully added."); + reply(nil); +} + +@end diff --git a/Daemon/SCDaemonProtocol.h b/Daemon/SCDaemonProtocol.h index fbb42409..3f4186bd 100644 --- a/Daemon/SCDaemonProtocol.h +++ b/Daemon/SCDaemonProtocol.h @@ -11,11 +11,11 @@ NS_ASSUME_NONNULL_BEGIN @protocol SCDaemonProtocol -- (BOOL) install; +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; - (BOOL) checkup; -- (BOOL) getVersion; +- (void)getVersionWithReply:(void(^)(NSString * version))reply; @end diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 751554b5..bd4b1022 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -6,12 +6,19 @@ // #import "SCDaemonXPC.h" +#import "version-header.h" +#import "SCDaemonBlockMethods.h" @implementation SCDaemonXPC -- (BOOL) install { - NSLog(@"XPC method called: install"); - return YES; +// TODO: CHECK AUTHORIZATION ON WRITE METHODS! + + +// TODO: make this run without dependence on the user or even settings - should just pass the blocklist right to the method +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"XPC method called: startBlockWithReply"); + + [SCDaemonBlockMethods startBlockWithControllingUID: controllingUID blocklist: blocklist endDate: endDate authorization: authData reply: reply]; } - (BOOL) checkup { @@ -19,10 +26,13 @@ - (BOOL) checkup { return YES; } -- (BOOL) getVersion { - NSLog(@"XPC method called: getVersion"); - return YES; +// Part of the HelperToolProtocol. Returns the version number of the tool. Note that never +// requires authorization. +- (void)getVersionWithReply:(void(^)(NSString * version))reply { + NSLog(@"XPC method called: getVersionWithReply"); + // We specifically don't check for authorization here. Everyone is always allowed to get + // the version of the helper tool. + reply(SELFCONTROL_VERSION_STRING); } - @end diff --git a/SCAppXPC.h b/SCAppXPC.h new file mode 100644 index 00000000..d2ba0982 --- /dev/null +++ b/SCAppXPC.h @@ -0,0 +1,23 @@ +// +// SCAppXPC.h +// SelfControl +// +// Created by Charlie Stigler on 7/4/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SCAppXPC : NSObject + +- (void)connectToHelperTool; +- (void)refreshConnection; +- (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock; + +- (void)getVersion; +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SCAppXPC.m b/SCAppXPC.m new file mode 100644 index 00000000..38ba1acb --- /dev/null +++ b/SCAppXPC.m @@ -0,0 +1,127 @@ +// +// SCAppXPC.m +// SelfControl +// +// Created by Charlie Stigler on 7/4/20. +// + +#import "SCAppXPC.h" +#import "SCDaemonProtocol.h" + +@interface SCAppXPC () {} + +@property (atomic, strong, readwrite) NSXPCConnection* daemonConnection; + +@end + +@implementation SCAppXPC + +// Ensures that we're connected to our helper tool +// should only be called from the main thread +// Copied from Apple's EvenBetterAuthorizationSample +- (void)connectToHelperTool { + assert([NSThread isMainThread]); + NSLog(@"Connecting to helper tool, daemon connection is %@", self.daemonConnection); + if (self.daemonConnection == nil) { + self.daemonConnection = [[NSXPCConnection alloc] initWithMachServiceName: @"org.eyebeam.selfcontrold" options: NSXPCConnectionPrivileged]; + self.daemonConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SCDaemonProtocol)]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-retain-cycles" + // We can ignore the retain cycle warning because a) the retain taken by the + // invalidation handler block is released by us setting it to nil when the block + // actually runs, and b) the retain taken by the block passed to -addOperationWithBlock: + // will be released when that operation completes and the operation itself is deallocated + // (notably self does not have a reference to the NSBlockOperation). + // note we need a local reference to the daemonConnection since there is a race condition where + // we could reinstantiate a new connection before the handler fires, and we don't want to clear the new connection + NSXPCConnection* connection = self.daemonConnection; + connection.invalidationHandler = ^{ + // If the connection gets invalidated then, on the main thread, nil out our + // reference to it. This ensures that we attempt to rebuild it the next time around. + connection.invalidationHandler = nil; + NSLog(@"called invalidation handler"); + + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + if (connection == self.daemonConnection) { + self.daemonConnection = nil; + NSLog(@"connection invalidated"); + } + }]; + }; + #pragma clang diagnostic pop + [self.daemonConnection resume]; + + NSLog(@"Started helper connection!"); + } +} +- (void)refreshConnection { + // when we're refreshing the connection, we can end up in a slightly awkward situation: + // if we call invalidate, but immediately start to reconnect before daemonConnection can be nil'd out + // then we risk trying to use the invalidated connection + // the fix? nil out daemonConnection before invalidating it in the refresh case + + NSXPCConnection* oldConnection = self.daemonConnection; + + // dispatch_sync on main thread would deadlock, so be careful + if ([NSThread isMainThread]) { + self.daemonConnection = nil; + } else { + // running this synchronously ensures that the daemonConnection is nil'd out even if + // reinstantiate the connection immediately + dispatch_sync(dispatch_get_main_queue(), ^{ + self.daemonConnection = nil; + }); + } + + [oldConnection performSelectorOnMainThread: @selector(invalidate) withObject: nil waitUntilDone: YES]; + +} + +// Also copied from Apple's EvenBetterAuthorizationSample +// Connects to the helper tool and then executes the supplied command block on the +// main thread, passing it an error indicating if the connection was successful. +- (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock { + // Ensure that there's a helper tool connection in place. + + [self performSelectorOnMainThread: @selector(connectToHelperTool) withObject:nil waitUntilDone: YES]; + + // Run the command block. Note that we never error in this case because, if there is + // an error connecting to the helper tool, it will be delivered to the error handler + // passed to -remoteObjectProxyWithErrorHandler:. However, I maintain the possibility + // of an error here to allow for future expansion. + + commandBlock(nil); +} + +// Called when the user clicks the Get Version button. This is the simplest form of +// NSXPCConnection request because it doesn't require any authorization. +- (void)getVersion { + [self connectAndExecuteCommandBlock:^(NSError * connectError) { + if (connectError != nil) { + NSLog(@"%@", connectError); + } else { + [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { + NSLog(@"%@", proxyError); + }] getVersionWithReply:^(NSString *version) { + NSLog(@"version = %@\n", version); + }]; + } + }]; +} + +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"sending command block"); + [self connectAndExecuteCommandBlock:^(NSError * connectError) { + if (connectError != nil) { + NSLog(@"Command failed with connection error: %@", connectError); + } else { + [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { + NSLog(@"Command failed with remote object proxy error: %@", proxyError); + }] startBlockWithControllingUID: controllingUID blocklist: blocklist endDate:endDate authorization: [NSData new] reply:^(NSError* error) { + NSLog(@"installed with error = %@\n", error); + }]; + } + }]; +} + +@end diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index ab5158d6..6df3a60e 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -40,6 +40,19 @@ CB5DFCBB2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB62FC3024B11A4F00ADBC40 /* selfcontrold-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */; }; + CB62FC3B24B124B900ADBC40 /* SCAppXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = CB62FC3A24B124B900ADBC40 /* SCAppXPC.m */; }; + CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */; }; + CB62FC3F24B1327A00ADBC40 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; + CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBF3B573217BADD7006D5F52 /* SCSettings.m */; }; + CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */; }; + CB62FC4224B1329200ADBC40 /* BlockManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806116C1FDBE0059C99A /* BlockManager.m */; }; + CB62FC4324B1329500ADBC40 /* PacketFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = CBCA91111960D87300AFD20C /* PacketFilter.m */; }; + CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; + CB62FC4524B1329F00ADBC40 /* ThunderbirdPreferenceParser.m in Sources */ = {isa = PBXBuildFile; fileRef = CBE4401A0F4BE0670062A1FE /* ThunderbirdPreferenceParser.m */; }; + CB62FC4624B132A300ADBC40 /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; + CB62FC4724B132A600ADBC40 /* AllowlistScraper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73615F19E4FDA000E0924F /* AllowlistScraper.m */; }; + CB62FC4824B132B300ADBC40 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; + CB62FC4924B1330700ADBC40 /* LaunchctlHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */; }; CB6840A5247A3E5500E51564 /* SelfControlKillerIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */; }; CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73615F19E4FDA000E0924F /* AllowlistScraper.m */; }; CB74D1152480E506002B2079 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB32D2AD21902D9D00B8CD68 /* IOKit.framework */; }; @@ -221,6 +234,10 @@ CB5DFCB52251DD1F0084CEC2 /* SCConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCConstants.h; sourceTree = ""; }; CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCConstants.m; sourceTree = ""; }; CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "selfcontrold-Info.plist"; sourceTree = ""; }; + CB62FC3924B124B900ADBC40 /* SCAppXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCAppXPC.h; sourceTree = ""; }; + CB62FC3A24B124B900ADBC40 /* SCAppXPC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCAppXPC.m; sourceTree = ""; }; + CB62FC3C24B1298500ADBC40 /* SCDaemonBlockMethods.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonBlockMethods.h; sourceTree = ""; }; + CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonBlockMethods.m; sourceTree = ""; }; CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = SelfControlKillerIcon.icns; sourceTree = ""; }; CB6ED5662085C70200012817 /* da */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = da; path = da.lproj/FirstTime.xib; sourceTree = ""; }; CB7026181CD7177400D7C7F0 /* fr */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -561,6 +578,8 @@ CBD4848D19D768C90020F949 /* PreferencesAdvancedViewController.m */, CBB637210F3E296000EBD135 /* DomainListWindowController.h */, CBB637220F3E296000EBD135 /* DomainListWindowController.m */, + CB62FC3924B124B900ADBC40 /* SCAppXPC.h */, + CB62FC3A24B124B900ADBC40 /* SCAppXPC.m */, CBD266C611ED82DB00042CD8 /* Helper Tools */, CBB0AE280FA74566006229B3 /* HostFileBlocker.h */, CBB0AE290FA74566006229B3 /* HostFileBlocker.m */, @@ -605,8 +624,11 @@ CB74D121248371E7002B2079 /* Daemon */ = { isa = PBXGroup; children = ( + CB74D1032480E4D9002B2079 /* DaemonMain.m */, CB74D0FC2480E3E6002B2079 /* SCDaemon.h */, CB74D0FD2480E3E6002B2079 /* SCDaemon.m */, + CB62FC3C24B1298500ADBC40 /* SCDaemonBlockMethods.h */, + CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */, CB8086D224837607004B88BD /* SCDaemonXPC.h */, CB8086D324837607004B88BD /* SCDaemonXPC.m */, CB74D122248374E6002B2079 /* SCDaemonProtocol.h */, @@ -660,7 +682,6 @@ CBD266C611ED82DB00042CD8 /* Helper Tools */ = { isa = PBXGroup; children = ( - CB74D1032480E4D9002B2079 /* DaemonMain.m */, CBA2AFD70F39EC46005AFEBE /* HelperMain.h */, CBA2AFD80F39EC46005AFEBE /* HelperMain.m */, CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */, @@ -1141,6 +1162,7 @@ F5B8CBEE19EE21C30026F3A5 /* SCTimeIntervalFormatter.m in Sources */, CBD4848A19D764440020F949 /* PreferencesGeneralViewController.m in Sources */, CBEE50C10F48C21F00F5DF1C /* TimerWindowController.m in Sources */, + CB62FC3B24B124B900ADBC40 /* SCAppXPC.m in Sources */, CB90BF830F49F430006D202D /* HostImporter.m in Sources */, CB5DFCB72251DD1F0084CEC2 /* SCConstants.m in Sources */, CBE4401B0F4BE0670062A1FE /* ThunderbirdPreferenceParser.m in Sources */, @@ -1168,8 +1190,20 @@ buildActionMask = 2147483647; files = ( CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */, + CB62FC4624B132A300ADBC40 /* HostImporter.m in Sources */, + CB62FC4724B132A600ADBC40 /* AllowlistScraper.m in Sources */, + CB62FC4524B1329F00ADBC40 /* ThunderbirdPreferenceParser.m in Sources */, CB74D1202480E566002B2079 /* SCDaemon.m in Sources */, + CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */, + CB62FC3F24B1327A00ADBC40 /* SCUtilities.m in Sources */, + CB62FC4924B1330700ADBC40 /* LaunchctlHelper.m in Sources */, + CB62FC4824B132B300ADBC40 /* SCConstants.m in Sources */, + CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */, + CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, + CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */, CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */, + CB62FC4324B1329500ADBC40 /* PacketFilter.m in Sources */, + CB62FC4224B1329200ADBC40 /* BlockManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 620dffce99b969dd08e0da698f544f110ed4b2a8 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Thu, 16 Jul 2020 23:37:02 -0700 Subject: [PATCH 06/72] GOD DAMN IT WASNT EVEN RELATED TO INVALIDATING CONNECTIONS --- AppController.m | 22 +++++++++++----------- SCAppXPC.h | 2 +- SCAppXPC.m | 46 ++++++++++++++++++++++++++++------------------ 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/AppController.m b/AppController.m index 1243aa28..071e9abd 100755 --- a/AppController.m +++ b/AppController.m @@ -351,7 +351,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // start up our daemon XPC self.xpc = [SCAppXPC new]; - [self.xpc connectToHelperTool]; + [self.xpc connectToHelperTool]; // Register observers on both distributed and normal notification centers // to receive notifications from the helper tool and the other parts of the @@ -721,22 +721,22 @@ - (void)installBlock { [self refreshUserInterface]; return; - } - + } else NSLog(@"succeeded in installing helper tool"); + + // ok, the new helper tool is installed! refresh the connection, then it's time to start the block - dispatch_async(dispatch_get_main_queue(), ^{ - [self.xpc refreshConnection]; + [self.xpc refreshConnectionAndRun:^{ NSLog(@"Refreshed connection!"); -// [self.xpc getVersion]; + // [self.xpc getVersion]; [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID - blocklist: [settings_ valueForKey: @"Blocklist"] - endDate: [settings_ valueForKey: @"BlockEndDate"] - authorization: nil + blocklist: [self->settings_ valueForKey: @"Blocklist"] + endDate: [self->settings_ valueForKey: @"BlockEndDate"] + authorization: [NSData new] reply:^(NSError * _Nonnull error) { NSLog(@"WOO started block with error %@", error); }]; - }); - +// [self.xpc getVersion]; + }]; // NSFileHandle* helperToolHandle = [[NSFileHandle alloc] initWithFileDescriptor: fileno(commPipe) closeOnDealloc: YES]; // // NSData* inData = [helperToolHandle readDataToEndOfFile]; diff --git a/SCAppXPC.h b/SCAppXPC.h index d2ba0982..4445e9f3 100644 --- a/SCAppXPC.h +++ b/SCAppXPC.h @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SCAppXPC : NSObject - (void)connectToHelperTool; -- (void)refreshConnection; +- (void)refreshConnectionAndRun:(void(^)(void))callback; - (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock; - (void)getVersion; diff --git a/SCAppXPC.m b/SCAppXPC.m index 38ba1acb..7050bccb 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -41,12 +41,21 @@ - (void)connectToHelperTool { connection.invalidationHandler = nil; NSLog(@"called invalidation handler"); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if (connection == self.daemonConnection) { + if (connection == self.daemonConnection) { + // dispatch_sync on main thread would deadlock, so be careful + if ([NSThread isMainThread]) { self.daemonConnection = nil; - NSLog(@"connection invalidated"); + NSLog(@"set daemonConnection to nil on main thread"); + } else { + // running this synchronously ensures that the daemonConnection is nil'd out even if + // reinstantiate the connection immediately + dispatch_sync(dispatch_get_main_queue(), ^{ + self.daemonConnection = nil; + NSLog(@"set daemonConnection to nil via dispatch_sync"); + }); } - }]; + NSLog(@"connection invalidated"); + } else NSLog(@"not invalidating connection cause they don't match"); }; #pragma clang diagnostic pop [self.daemonConnection resume]; @@ -54,26 +63,27 @@ - (void)connectToHelperTool { NSLog(@"Started helper connection!"); } } -- (void)refreshConnection { +- (void)refreshConnectionAndRun:(void(^)(void))callback { // when we're refreshing the connection, we can end up in a slightly awkward situation: // if we call invalidate, but immediately start to reconnect before daemonConnection can be nil'd out // then we risk trying to use the invalidated connection // the fix? nil out daemonConnection before invalidating it in the refresh case + NSLog(@"refresh connection"); - NSXPCConnection* oldConnection = self.daemonConnection; - - // dispatch_sync on main thread would deadlock, so be careful - if ([NSThread isMainThread]) { - self.daemonConnection = nil; - } else { - // running this synchronously ensures that the daemonConnection is nil'd out even if - // reinstantiate the connection immediately - dispatch_sync(dispatch_get_main_queue(), ^{ - self.daemonConnection = nil; - }); + if (self.daemonConnection == nil) { + NSLog(@"no daemon connection to invalidate"); + callback(); + return; } - - [oldConnection performSelectorOnMainThread: @selector(invalidate) withObject: nil waitUntilDone: YES]; + void (^standardInvalidationHandler)(void) = self.daemonConnection.invalidationHandler; + + // wait until the invalidation handler runs, then run our callback + self.daemonConnection.invalidationHandler = ^{ + standardInvalidationHandler(); + callback(); + }; + + [self.daemonConnection performSelectorOnMainThread: @selector(invalidate) withObject: nil waitUntilDone: YES]; } From 42bc80f25e8df3ef4233614d7510eb72c7355d74 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Wed, 16 Sep 2020 22:19:28 -0600 Subject: [PATCH 07/72] Blocks basically start and stop --- Daemon/SCDaemon.m | 6 ++- Daemon/SCDaemonBlockMethods.h | 2 + Daemon/SCDaemonBlockMethods.m | 78 ++++++++++++++++++++++++++- Daemon/SCDaemonUtilities.h | 18 +++++++ Daemon/SCDaemonUtilities.m | 45 ++++++++++++++++ HelperCommon.m | 41 +++++++------- SCAppXPC.m | 1 + SelfControl.xcodeproj/project.pbxproj | 19 ++++--- 8 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 Daemon/SCDaemonUtilities.h create mode 100644 Daemon/SCDaemonUtilities.m diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 5d6bd002..2c1a48bc 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -8,6 +8,7 @@ #import "SCDaemon.h" #import "SCDaemonProtocol.h" #import "SCDaemonXPC.h" +#import"SCDaemonBlockMethods.h" static NSString* serviceName = @"org.eyebeam.selfcontrold"; @@ -28,8 +29,9 @@ - (id) init { - (void)start { [self.listener resume]; - NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 15 repeats: YES block:^(NSTimer * _Nonnull timer) { - NSLog(@"still running"); + NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { + // TODO: DON'T HARDCODE THIS VALUE + [SCDaemonBlockMethods checkupBlockWithControllingUID: 501]; }]; } diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index 55df7ae6..70afc73a 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; ++ (void)checkupBlockWithControllingUID:(uid_t)controllingUID; + @end NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index c35584dc..511d2d84 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -8,6 +8,8 @@ #import "SCDaemonBlockMethods.h" #import "SCSettings.h" #import "HelperCommon.h" +#import "PacketFilter.h" +#import "SCDaemonUtilities.h" NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; @@ -30,13 +32,13 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray + +NS_ASSUME_NONNULL_BEGIN + +@interface SCDaemonUtilities : NSObject + ++ (void)unloadDaemonJobForUID:(uid_t)controllingUID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m new file mode 100644 index 00000000..daea8b7f --- /dev/null +++ b/Daemon/SCDaemonUtilities.m @@ -0,0 +1,45 @@ +// +// SCDaemonUtilities.m +// org.eyebeam.selfcontrold +// +// Created by Charlie Stigler on 9/16/20. +// + +#import "SCDaemonUtilities.h" +#import +#import "SCSettings.h" + +@implementation SCDaemonUtilities + ++ (void)unloadDaemonJobForUID:(uid_t)controllingUID { + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + + // we're about to unload the launchd job + // this will kill this process, so we have to make sure + // all settings are synced before we unload + [settings synchronizeSettingsWithCompletion:^(NSError* err) { + if (err != nil) { + NSLog(@"WARNING: Settings failed to synchronize before unloading daemon, with error %@", err); + } + + CFErrorRef cfError; + SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); + if (cfError) { + NSLog(@"Failed to remove selfcontrold daemon with error %@", cfError); + } + }]; + + // wait 5 seconds. assuming the synchronization completes during that time, + // it'll unload the launchd job for us and we'll never get to the other side of this wait + sleep(5); + + // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. + NSLog(@"WARNING: Settings sync timed out before unloading block"); + CFErrorRef cfError; + SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); + if (cfError) { + NSLog(@"Failed to remove selfcontrold daemon with error %@", cfError); + } +} + +@end diff --git a/HelperCommon.m b/HelperCommon.m index 3b830db1..0f353df9 100644 --- a/HelperCommon.m +++ b/HelperCommon.m @@ -12,6 +12,7 @@ #import "SCUtilities.h" #import "SCSettings.h" #import "SCConstants.h" +#import BOOL blockIsRunningInSettingsOrDefaults(uid_t controllingUID) { SCSettings* settings = [SCSettings settingsForUser: controllingUID]; @@ -202,24 +203,28 @@ void removeBlock(uid_t controllingUID) { clearCachesIfRequested(controllingUID); - // the final step is to unload the launchd job - // this will kill this process, so we have to make sure - // all settings are synced before we unload - [settings synchronizeSettingsWithCompletion:^(NSError* err) { - if (err != nil) { - NSLog(@"WARNING: Settings failed to synchronize before unloading block, with error %@", err); - } - - [LaunchctlHelper unloadLaunchdJobWithPlistAt:@"/Library/LaunchDaemons/org.eyebeam.SelfControl.plist"]; - }]; - - // wait 5 seconds. assuming the synchronization completes during that time, - // it'll unload the launchd job for us and we'll never get to the other side of this wait - sleep(5); - - // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. - NSLog(@"WARNING: Settings sync timed out before unloading block"); - [LaunchctlHelper unloadLaunchdJobWithPlistAt:@"/Library/LaunchDaemons/org.eyebeam.SelfControl.plist"]; +// // the final step is to unload the launchd job +// // this will kill this process, so we have to make sure +// // all settings are synced before we unload +// [settings synchronizeSettingsWithCompletion:^(NSError* err) { +// if (err != nil) { +// NSLog(@"WARNING: Settings failed to synchronize before unloading block, with error %@", err); +// } +// +// CFErrorRef cfError; +// SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); +// NSLog(@"ran SMJobRemove and removed job with error %@", cfError); +// }]; +// +// // wait 5 seconds. assuming the synchronization completes during that time, +// // it'll unload the launchd job for us and we'll never get to the other side of this wait +// sleep(5); +// +// // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. +// NSLog(@"WARNING: Settings sync timed out before unloading block"); +// CFErrorRef cfError; +// SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); +// NSLog(@"ran SMJobRemove and removed job with error %@", cfError); } void sendConfigurationChangedNotification() { diff --git a/SCAppXPC.m b/SCAppXPC.m index 7050bccb..a5e54a87 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -125,6 +125,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray \"${PROJECT_DIR}/version-header.h\"\n"; }; - - CBFF218A3108765AAB5DE542 /* [CP] Copy Pods Resources */ = { + CBFF218A3108765AAB5DE542 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1190,6 +1192,7 @@ buildActionMask = 2147483647; files = ( CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */, + CB850F3925130F5300EE2E2D /* NSString+IPAddress.m in Sources */, CB62FC4624B132A300ADBC40 /* HostImporter.m in Sources */, CB62FC4724B132A600ADBC40 /* AllowlistScraper.m in Sources */, CB62FC4524B1329F00ADBC40 /* ThunderbirdPreferenceParser.m in Sources */, @@ -1199,6 +1202,7 @@ CB62FC4924B1330700ADBC40 /* LaunchctlHelper.m in Sources */, CB62FC4824B132B300ADBC40 /* SCConstants.m in Sources */, CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */, + CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */, CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */, CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */, @@ -1269,11 +1273,6 @@ target = CB9C811A19CFBA8500CDCAE1 /* SCKillerHelper */; targetProxy = CB9C812D19CFBBA300CDCAE1 /* PBXContainerItemProxy */; }; - CB9F02F316C10CE6003054EE /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 8D1107260486CEB800E47090 /* SelfControl */; - targetProxy = CB9F02F216C10CE6003054EE /* PBXContainerItemProxy */; - }; CBE2BB5519D10CBB0077124F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CB9C811A19CFBA8500CDCAE1 /* SCKillerHelper */; From 679c679e165e64b16d9e6f76dfcd43c4f7c68f95 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 26 Oct 2020 19:40:25 -0700 Subject: [PATCH 08/72] WIP moving to global secured settings --- AppController.m | 45 +++++++++++------------- Daemon/SCDaemonBlockMethods.m | 4 +-- DomainListWindowController.m | 12 +++---- HelperCommon.m | 2 +- HelperMain.m | 4 +-- SCAppXPC.m | 10 +++--- SCSettings.m | 64 +++++++++++++++++++++++++++++++---- SCUtilities.m | 3 +- 8 files changed, 97 insertions(+), 47 deletions(-) diff --git a/AppController.m b/AppController.m index 071e9abd..049d016f 100755 --- a/AppController.m +++ b/AppController.m @@ -53,6 +53,7 @@ - (AppController*) init { settings_ = [SCSettings currentUserSettings]; NSDictionary* appDefaults = @{ + @"Blocklist": @[], @"HighlightInvalidHosts": @YES, @"VerifyInternetConnection": @YES, @"TimerWindowFloats": @NO, @@ -118,7 +119,7 @@ - (IBAction)updateTimeSliderDisplay:(id)sender { NSString* timeString = [self timeSliderDisplayStringFromNumberOfMinutes:numMinutes]; [blockSliderTimeDisplayLabel_ setStringValue:timeString]; - [submitButton_ setEnabled: (numMinutes > 0) && ([[settings_ valueForKey: @"Blocklist"] count] > 0)]; + [submitButton_ setEnabled: (numMinutes > 0) && ([[defaults_ arrayForKey: @"Blocklist"] count] > 0)]; } - (NSString *)timeSliderDisplayStringFromNumberOfMinutes:(NSInteger)numberOfMinutes { @@ -159,7 +160,7 @@ - (IBAction)addBlock:(id)sender { [NSApp presentError: err]; return; } - if([[settings_ valueForKey:@"Blocklist"] count] == 0) { + if([[defaults_ arrayForKey: @"Blocklist"] count] == 0) { // Since the Start button should be disabled when the blocklist has no entries, // this should definitely not be happening. Exit. @@ -248,7 +249,7 @@ - (void)refreshUserInterface { BOOL addBlockIsOngoing = self.addingBlock; - if([defaults_ integerForKey: @"BlockDuration"] != 0 && [[settings_ valueForKey: @"Blocklist"] count] != 0 && !addBlockIsOngoing) { + if([defaults_ integerForKey: @"BlockDuration"] != 0 && [[defaults_ arrayForKey: @"Blocklist"] count] != 0 && !addBlockIsOngoing) { [submitButton_ setEnabled: YES]; } else { [submitButton_ setEnabled: NO]; @@ -407,16 +408,7 @@ - (BOOL)blockIsRunning { return [SCUtilities blockIsRunningWithSettings: settings_ defaults: defaults_]; } -- (IBAction)showDomainList:(id)sender { - [self.xpc getVersion]; - [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID - blocklist: [settings_ valueForKey: @"Blocklist"] - endDate: [settings_ valueForKey: @"BlockEndDate"] - authorization: nil - reply:^(NSError * _Nonnull error) { - NSLog(@"WOO started block with error %@", error); - }]; - +- (IBAction)showDomainList:(id)sender { BOOL addBlockIsOngoing = self.addingBlock; if([self blockIsRunning] || addBlockIsOngoing) { NSAlert* blockInProgressAlert = [[NSAlert alloc] init]; @@ -463,7 +455,7 @@ - (BOOL)networkConnectionIsAvailable { } - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { - NSMutableArray* list = [[settings_ valueForKey: @"Blocklist"] mutableCopy]; + NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; NSArray* cleanedEntries = [SCUtilities cleanBlocklistEntry: host]; if (cleanedEntries.count == 0) return; @@ -473,7 +465,7 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { [list addObject: entry]; } - [settings_ setValue: list forKey: @"Blocklist"]; + [settings_ setValue: list forKey: @"ActiveBlocklist"]; if(![self blockIsRunning]) { // This method shouldn't be getting called, a block is not on. @@ -483,9 +475,9 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { [self refreshUserInterface]; // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"Blocklist"] mutableCopy]; + NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; [list removeLastObject]; - [settings_ setValue: list forKey: @"Blocklist"]; + [settings_ setValue: list forKey: @"ActiveBlocklist"]; NSError* err = [NSError errorWithDomain:kSelfControlErrorDomain code: -103 @@ -505,9 +497,9 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { if([networkUnavailableAlert runModal] == NSAlertFirstButtonReturn) { // User clicked cancel // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"Blocklist"] mutableCopy]; + NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; [list removeLastObject]; - [settings_ setValue: list forKey: @"Blocklist"]; + [settings_ setValue: list forKey: @"ActiveBlocklist"]; return; } @@ -519,9 +511,9 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { CFNetDiagnosticDiagnoseProblemInteractively(diagRef); // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"Blocklist"] mutableCopy]; + NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; [list removeLastObject]; - [settings_ setValue: list forKey: @"Blocklist"]; + [settings_ setValue: list forKey: @"ActiveBlocklist"]; return; } @@ -729,11 +721,14 @@ - (void)installBlock { NSLog(@"Refreshed connection!"); // [self.xpc getVersion]; [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID - blocklist: [self->settings_ valueForKey: @"Blocklist"] + blocklist: [self->defaults_ arrayForKey: @"Blocklist"] endDate: [self->settings_ valueForKey: @"BlockEndDate"] authorization: [NSData new] reply:^(NSError * _Nonnull error) { NSLog(@"WOO started block with error %@", error); + + self.addingBlock = false; + [self refreshUserInterface]; }]; // [self.xpc getVersion]; }]; @@ -810,9 +805,9 @@ - (void)refreshBlock:(NSLock*)lockToUse { NSLog(@"ERROR: Failed to authorize block refresh."); // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"Blocklist"] mutableCopy]; + NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; [list removeLastObject]; - [settings_ setValue: list forKey: @"Blocklist"]; + [settings_ setValue: list forKey: @"ActiveBlocklist"]; [lockToUse unlock]; @@ -973,7 +968,7 @@ - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename NSNumber* newAllowlistChoice = openedDict[@"BlockAsWhitelist"]; if(newBlocklist == nil || newAllowlistChoice == nil) return NO; - [settings_ setValue: newBlocklist forKey: @"Blocklist"]; + [defaults_ setValue: newBlocklist forKey:@"Blocklist"]; [settings_ setValue: newAllowlistChoice forKey: @"BlockAsWhitelist"]; BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible]; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 511d2d84..b5ec7de3 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -32,8 +32,8 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray* libraryURLs = [[NSFileManager defaultManager] URLsForDirectory: NSLibraryDirectory inDomains: NSLocalDomainMask]; + NSLog(@"secured settings file path got lib URLs: %@", libraryURLs); + + return [NSString stringWithFormat: @"/Users/Shared/%@", [self settingsFileName]]; +} +- (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId { + NSString* homeDir = [self homeDirectoryForUid: userId]; + return [[NSString stringWithFormat: @"%@/Library/Preferences/%@", homeDir, [self settingsFileName]] stringByExpandingTildeInPath]; } // NOTE: there should be a default setting for each valid setting, even if it's nil/zero/etc - (NSDictionary*)defaultSettingsDict { return @{ @"BlockEndDate": [NSDate distantPast], - @"Blocklist": @[], + @"ActiveBlocklist": @[], @"EvaluateCommonSubdomains": @YES, @"IncludeLinkedDomains": @YES, @"BlockSoundShouldPlay": @NO, @@ -261,7 +275,7 @@ - (void)writeSettingsWithCompletion:(nullable void(^)(NSError* _Nullable))comple BOOL chmodSuccessful = [[NSFileManager defaultManager] setAttributes: @{ @"NSFileOwnerAccountID": [NSNumber numberWithUnsignedLong: self.userId], - @"NSFilePosixPermissions": [NSNumber numberWithShort: 0755] + @"NSFilePosixPermissions": [NSNumber numberWithShort: 0777] } ofItemAtPath: self.securedSettingsFilePath error: &chmodErr]; @@ -384,6 +398,44 @@ - (id)valueForKey:(NSString*)key { // blocks are started or finished. // NOTE3: this method always pulls user defaults for the current user, regardless of what instance it's called on - (void)migrateLegacySettings { + // try to migrate from the user-based secured settings model (v3.0-3.0.3) + // basically, we're gonna take the most-recently-updated settings, if they exist + // of course, we can only access settings that are readable to us, i.e. + // if this first gets run as user X, they generally won't be able to read user Y's prefs + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSArray* libraryURLs = [fileManager URLsForDirectory: NSLibraryDirectory inDomains: NSAllDomainsMask]; + NSMutableArray* preferencePaths = [NSMutableArray arrayWithCapacity: libraryURLs.count]; + for (NSURL* libraryURL in libraryURLs) { + [preferencePaths addObject: [NSString stringWithFormat: @"%@/Preferences/%@", libraryURL.path, [self settingsFileName]]]; + } + NSDictionary* latestSettingsDict; + for (NSString* prefPath in preferencePaths) { + if ([fileManager isReadableFileAtPath: prefPath]) { + + NSDictionary* settingsFromDisk = [NSDictionary dictionaryWithContentsOfFile: prefPath]; + if (!settingsFromDisk) continue; + + if (latestSettingsDict == nil || [settingsFromDisk[@"LastSettingsUpdate"] timeIntervalSinceDate: latestSettingsDict[@"LastSettingsUpdate"]] > 0) { + latestSettingsDict = settingsFromDisk; + } + } + } + + if (latestSettingsDict != nil) { + NSLog(@"Migrating all settings from %@", latestSettingsDict); +// for (NSString* key in [[self defaultSettingsDict] allKeys]) { +// if (latestSettingsDict[key] != nil) { +// [self setValue: latestSettingsDict[key] forKey: key]; +// } +// } + NSLog(@"Migrated!"); + return; + } + + + // if no user-based secured settings exist, we try to read from the even older legacy defaults settings + + NSDictionary* lockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLegacyLockFilePath]; // note that the defaults will generally only be defined in the main app, not helper tool (because helper tool runs as root) NSDictionary* userDefaultsDict = [NSUserDefaults standardUserDefaults].dictionaryRepresentation; diff --git a/SCUtilities.m b/SCUtilities.m index 22e26839..8c76b735 100644 --- a/SCUtilities.m +++ b/SCUtilities.m @@ -201,7 +201,8 @@ + (NSDate*) endDateFromLegacyBlockDictionary:(NSDictionary *)dict { } + (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL settings:(SCSettings*)settings errorDescription:(NSString**)errDescriptionRef { - NSDictionary* saveDict = @{@"HostBlacklist": [settings valueForKey: @"Blocklist"], + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary* saveDict = @{@"HostBlacklist": [defaults arrayForKey: @"Blocklist"], @"BlockAsWhitelist": [settings valueForKey: @"BlockAsWhitelist"]}; NSString* saveDataErr; From 424e216bbfed26cca90bf3489c4b350f2eb51e26 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 29 Dec 2020 23:35:16 -0800 Subject: [PATCH 09/72] Add ActiveBlockAsWhitelist, doesnt totally work yet tho --- AppController.m | 14 +++++++++++--- Daemon/SCDaemonBlockMethods.h | 2 +- Daemon/SCDaemonBlockMethods.m | 6 ++++-- Daemon/SCDaemonProtocol.h | 2 +- Daemon/SCDaemonXPC.m | 4 ++-- DomainListWindowController.m | 8 ++++---- HelperCommon.m | 4 ++-- HelperMain.m | 9 ++++++--- SCAppXPC.h | 2 +- SCAppXPC.m | 4 ++-- SCSettings.m | 36 +++++++++++++++++++++++++---------- SCUtilities.h | 2 +- SCUtilities.m | 16 +++++++++------- TimerWindowController.m | 2 +- 14 files changed, 71 insertions(+), 40 deletions(-) diff --git a/AppController.m b/AppController.m index 049d016f..8c3a2794 100755 --- a/AppController.m +++ b/AppController.m @@ -290,7 +290,7 @@ - (void)refreshUserInterface { } // Display "blocklist" or "allowlist" as appropriate - NSString* listType = [[settings_ valueForKey: @"BlockAsWhitelist"] boolValue] ? @"Allowlist" : @"Blocklist"; + NSString* listType = [defaults_ boolForKey: @"BlockAsWhitelist"] ? @"Allowlist" : @"Blocklist"; NSString* editListString = NSLocalizedString(([NSString stringWithFormat: @"Edit %@", listType]), @"Edit list button / menu item"); editBlocklistButton_.title = editListString; @@ -722,6 +722,7 @@ - (void)installBlock { // [self.xpc getVersion]; [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID blocklist: [self->defaults_ arrayForKey: @"Blocklist"] + isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"] endDate: [self->settings_ valueForKey: @"BlockEndDate"] authorization: [NSData new] reply:^(NSError * _Nonnull error) { @@ -944,7 +945,14 @@ - (IBAction)open:(id)sender { long result = [oPanel runModal]; if (result == NSOKButton) { if([oPanel.URLs count] > 0) { - [SCUtilities readBlocklistFromFile: oPanel.URLs[0] toSettings: settings_]; + NSDictionary* settingsFromFile = [SCUtilities readBlocklistFromFile: oPanel.URLs[0]]; + + if (settingsFromFile != nil) { + [defaults_ setObject: settingsFromFile[@"Blocklist"] forKey: @"Blocklist"]; + [defaults_ setObject: settingsFromFile[@"BlockAsWhitelist"] forKey: @"BlockAsWhitelist"]; + } else { + NSLog(@"WARNING: Could not read a valid blocklist from file - ignoring."); + } // close the domain list (and reopen again if need be to refresh) BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible]; @@ -969,7 +977,7 @@ - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename if(newBlocklist == nil || newAllowlistChoice == nil) return NO; [defaults_ setValue: newBlocklist forKey:@"Blocklist"]; - [settings_ setValue: newAllowlistChoice forKey: @"BlockAsWhitelist"]; + [defaults_ setObject: newAllowlistChoice forKey: @"BlockAsWhitelist"]; BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible]; NSRect frame = [[domainListWindowController_ window] frame]; diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index 70afc73a..d5ac1d77 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SCDaemonBlockMethods : NSObject -+ (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; ++ (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + (void)checkupBlockWithControllingUID:(uid_t)controllingUID; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index b5ec7de3..40aff9a0 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -15,7 +15,7 @@ @implementation SCDaemonBlockMethods -+ (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { ++ (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { NSLog(@"startign block in methods"); if (blockIsRunningInSettingsOrDefaults(controllingUID)) { NSLog(@"ERROR: Block is already running"); @@ -34,7 +34,9 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray -- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; - (BOOL) checkup; diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index bd4b1022..15a0d1c1 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -15,10 +15,10 @@ @implementation SCDaemonXPC // TODO: make this run without dependence on the user or even settings - should just pass the blocklist right to the method -- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { NSLog(@"XPC method called: startBlockWithReply"); - [SCDaemonBlockMethods startBlockWithControllingUID: controllingUID blocklist: blocklist endDate: endDate authorization: authData reply: reply]; + [SCDaemonBlockMethods startBlockWithControllingUID: controllingUID blocklist: blocklist isAllowlist:isAllowlist endDate: endDate authorization: authData reply: reply]; } - (BOOL) checkup { diff --git a/DomainListWindowController.m b/DomainListWindowController.m index e0bc7dff..51cb1a82 100755 --- a/DomainListWindowController.m +++ b/DomainListWindowController.m @@ -45,7 +45,7 @@ - (DomainListWindowController*)init { return self; } - (void)awakeFromNib { - NSInteger indexToSelect = [[settings_ valueForKey: @"BlockAsWhitelist"] boolValue] ? 1 : 0; + NSInteger indexToSelect = [defaults_ boolForKey: @"BlockAsWhitelist"] ? 1 : 0; [allowlistRadioMatrix_ selectCellAtRow: indexToSelect column: 0]; [self updateWindowTitle]; @@ -236,11 +236,11 @@ - (void)tableView:(NSTableView *)tableView - (IBAction)allowlistOptionChanged:(NSMatrix*)sender { switch (sender.selectedRow) { case 0: - [settings_ setValue: @NO forKey: @"BlockAsWhitelist"]; + [defaults_ setBool: NO forKey: @"BlockAsWhitelist"]; break; case 1: [self showAllowlistWarning]; - [settings_ setValue: @YES forKey: @"BlockAsWhitelist"]; + [defaults_ setBool: YES forKey: @"BlockAsWhitelist"]; break; } @@ -264,7 +264,7 @@ - (void)showAllowlistWarning { } - (void)updateWindowTitle { - NSString* listType = [[settings_ valueForKey: @"BlockAsWhitelist"] boolValue] ? @"Allowlist" : @"Blocklist"; + NSString* listType = [defaults_ boolForKey: @"BlockAsWhitelist"] ? @"Allowlist" : @"Blocklist"; self.window.title = NSLocalizedString(([NSString stringWithFormat: @"Domain %@", listType]), @"Domain list window title"); } diff --git a/HelperCommon.m b/HelperCommon.m index 6e50ba3c..4978808a 100644 --- a/HelperCommon.m +++ b/HelperCommon.m @@ -39,8 +39,8 @@ void addRulesToFirewall(uid_t controllingUID) { BOOL allowLocalNetworks = [[settings valueForKey: @"AllowLocalNetworks"] boolValue]; BOOL includeLinkedDomains = [[settings valueForKey: @"IncludeLinkedDomains"] boolValue]; - // get value for BlockAsWhitelist - BOOL blockAsAllowlist = [[settings valueForKey: @"BlockAsWhitelist"] boolValue]; + // get value for ActiveBlockAsWhitelist + BOOL blockAsAllowlist = [[settings valueForKey: @"ActiveBlockAsWhitelist"] boolValue]; BlockManager* blockManager = [[BlockManager alloc] initAsAllowlist: blockAsAllowlist allowLocal: allowLocalNetworks includeCommonSubdomains: shouldEvaluateCommonSubdomains includeLinkedDomains: includeLinkedDomains]; diff --git a/HelperMain.m b/HelperMain.m index 145dacee..1e2d424b 100755 --- a/HelperMain.m +++ b/HelperMain.m @@ -91,13 +91,16 @@ int main(int argc, char* argv[]) { syncSettingsAndExit(settings, EX_IOERR); } else { [settings setValue: blockEndDateArg forKey: @"BlockEndDate"]; - BOOL readSuccess = [SCUtilities readBlocklistFromFile: [NSURL fileURLWithPath: pathToBlocklistFile] toSettings: settings]; + NSDictionary* readProperties = [SCUtilities readBlocklistFromFile: [NSURL fileURLWithPath: pathToBlocklistFile]]; - if (!readSuccess) { + if (readProperties == nil) { NSLog(@"ERROR: Block could not be read from file %@", pathToBlocklistFile); printStatus(-221); syncSettingsAndExit(settings, EX_IOERR); } + + [settings setValue: readProperties[@"Blocklist"] forKey: @"ActiveBlocklist"]; + [settings setValue: readProperties[@"BlockAsWhitelist"] forKey: @"ActiveBlockAsWhitelist"]; } } @@ -268,7 +271,7 @@ int main(int argc, char* argv[]) { // settings just in case. PacketFilter* pf = [[PacketFilter alloc] init]; HostFileBlocker* hostFileBlocker = [[HostFileBlocker alloc] init]; - if(![pf containsSelfControlBlock] || (![[settings valueForKey: @"BlockAsWhitelist"] boolValue] && ![hostFileBlocker containsSelfControlBlock])) { + if(![pf containsSelfControlBlock] || (![[settings valueForKey: @"ActiveBlockAsWhitelist"] boolValue] && ![hostFileBlocker containsSelfControlBlock])) { // The firewall is missing at least the block header. Let's clear everything // before we re-add to make sure everything goes smoothly. diff --git a/SCAppXPC.h b/SCAppXPC.h index 4445e9f3..9e562c73 100644 --- a/SCAppXPC.h +++ b/SCAppXPC.h @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock; - (void)getVersion; -- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; @end diff --git a/SCAppXPC.m b/SCAppXPC.m index 04bb320c..48186454 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -119,7 +119,7 @@ - (void)getVersion { }]; } -- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { NSLog(@"sending command block"); [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { @@ -129,7 +129,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray Date: Sat, 2 Jan 2021 21:19:42 -0800 Subject: [PATCH 10/72] checkpoint: fixed allowlist blocks, working on add-to-blocklist refactor --- AppController.m | 10 +- BlockManager.h | 1 + BlockManager.m | 142 +++++++++++------ Daemon/SCDaemonBlockMethods.m | 230 +++++++++++++++------------- Daemon/SCDaemonUtilities.m | 45 ++++++ Daemon/selfcontrold-Info.plist | 5 + HelperCommon.m | 14 +- HelperMain.m | 2 +- PacketFilter.h | 1 + PacketFilter.m | 99 +++++++++--- PreferencesAdvancedViewController.m | 8 +- PreferencesGeneralViewController.m | 4 +- SCAppXPC.m | 2 +- SCSettings.h | 1 + SCSettings.m | 3 + SCUtilities.h | 2 +- SCUtilities.m | 7 +- 17 files changed, 369 insertions(+), 207 deletions(-) diff --git a/AppController.m b/AppController.m index 8c3a2794..c64a0292 100755 --- a/AppController.m +++ b/AppController.m @@ -276,7 +276,7 @@ - (void)refreshUserInterface { // finally: if the helper tool marked that it detected tampering, make sure // we follow through and set the cheater wallpaper (helper tool can't do it itself) - if ([[settings_ valueForKey: @"TamperingDetected"] boolValue]) { + if ([settings_ boolForKey: @"TamperingDetected"]) { NSURL* cheaterBackgroundURL = [[NSBundle mainBundle] URLForResource: @"cheater-background" withExtension: @"png"]; NSArray* screens = [NSScreen screens]; for (NSScreen* screen in screens) { @@ -926,7 +926,13 @@ - (IBAction)save:(id)sender { /* if successful, save file under designated name */ if (runResult == NSOKButton) { NSString* errDescription; - [SCUtilities writeBlocklistToFileURL: sp.URL settings: settings_ errorDescription: &errDescription]; + [SCUtilities writeBlocklistToFileURL: sp.URL + blockInfo: @{ + @"Blocklist": [defaults_ arrayForKey: @"Blocklist"], + @"BlockAsWhitelist": [defaults_ objectForKey: @"BlockAsWhitelist"] + + } + errorDescription: &errDescription]; if(errDescription) { NSError* displayErr = [NSError errorWithDomain: kSelfControlErrorDomain code: -902 userInfo: @{NSLocalizedDescriptionKey: [@"Error 902: " stringByAppendingString: errDescription]}]; diff --git a/BlockManager.h b/BlockManager.h index 00f96be7..052309cb 100644 --- a/BlockManager.h +++ b/BlockManager.h @@ -41,6 +41,7 @@ - (BlockManager*)initAsAllowlist:(BOOL)allowlist allowLocal:(BOOL)local includeCommonSubdomains:(BOOL)blockCommon; - (BlockManager*)initAsAllowlist:(BOOL)allowlist allowLocal:(BOOL)local includeCommonSubdomains:(BOOL)blockCommon includeLinkedDomains:(BOOL)includeLinked; +- (void)enterAppendMode; - (void)prepareToAddBlock; - (void)finalizeBlock; - (void)addBlockEntryFromString:(NSString*)entry; diff --git a/BlockManager.m b/BlockManager.m index 4bf7ea80..93e65ca9 100644 --- a/BlockManager.m +++ b/BlockManager.m @@ -72,8 +72,18 @@ - (void)prepareToAddBlock { } } +- (void)enterAppendMode { + if(![hostsBlocker containsSelfControlBlock]) { + NSLog(@"ERROR: can't append to hosts block that doesn't yet exist"); + return; + } + [hostsBlocker enterAppendMode]; +} + - (void)finalizeBlock { + NSLog(@"About to run operation queue..."); [opQueue waitUntilAllOperationsAreFinished]; + NSLog(@"Operation queue ran!"); if(hostsBlockingEnabled) { [hostsBlocker addSelfControlBlockFooter]; @@ -116,48 +126,25 @@ - (void)addBlockEntryWithHostName:(NSString*)hostName port:(int)portNum maskLen: } - (void)addBlockEntryFromString:(NSString*)entry { - // don't do anything with blank hostnames, however they got on the list... - // otherwise they could end up screwing up the block - if (![[entry stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] length]) { - return; - } - NSDictionary* hostInfo = [self parseHostString: entry]; + // nil means that we don't have anything valid to block in this entry + if (hostInfo == nil) return; + NSString* hostName = hostInfo[@"hostName"]; - NSNumber* portNumObject = hostInfo[@"port"]; - NSNumber* maskLenObject = hostInfo[@"maskLen"]; - int portNum = portNumObject ? [portNumObject intValue] : 0; - int maskLen = maskLenObject ? [maskLenObject intValue] : 0; - - // we won't block host * (everywhere) without a port number... it's just too likely to be mistaken. - // Use a allowlist if that's what you want! - if ([hostName isEqualToString: @"*"] && !portNum) { - return; - } + int portNum = [hostInfo[@"port"] intValue]; + int maskLen = [hostInfo[@"maskLen"] intValue]; [self addBlockEntryWithHostName: hostName port: portNum maskLen: maskLen]; - - if (isAllowlist && includeLinkedDomains && ![hostName isValidIPAddress]) { - NSSet* relatedDomains = [AllowlistScraper relatedDomains: hostName]; - [relatedDomains enumerateObjectsUsingBlock:^(NSString* host, BOOL* stop) { - [self enqueueBlockEntryWithHostName: host port: 0 maskLen: 0]; - }]; - } - - if(![hostName isValidIPAddress] && includeCommonSubdomains) { - NSArray* commonSubdomains = [self commonSubdomainsForHostName: hostName]; - - for(int i = 0; i < [commonSubdomains count]; i++) { - // we do not pull port, we leave the port number the same as we got it - hostInfo = [self parseHostString: commonSubdomains[i]]; - hostName = hostInfo[@"hostName"]; - maskLenObject = hostInfo[@"maskLen"]; - maskLen = maskLenObject ? [maskLenObject intValue] : 0; - - [self enqueueBlockEntryWithHostName: hostName port: portNum maskLen: maskLen]; - } - } + + NSArray* relatedEntries = [self relatedBlockEntriesForEntry: hostInfo]; + for (NSDictionary* relatedHostInfo in relatedEntries) { + NSString* relatedHostName = hostInfo[@"hostName"]; + int relatedPortNum = [hostInfo[@"port"] intValue]; + int relatedMaskLen = [hostInfo[@"maskLen"] intValue]; + + [self enqueueBlockEntryWithHostName: relatedHostName port: relatedPortNum maskLen: relatedMaskLen]; + } } - (void)addBlockEntries:(NSArray*)blockList { @@ -169,6 +156,24 @@ - (void)addBlockEntries:(NSArray*)blockList { } } +- (void)appendBlockEntriesToRunningBlock:(NSArray*)addedBlocklistStrings { + /// no operations queue here, presumably we didn't add too much so it's OK to just do it in one loop + for (NSString* addedBlockString in addedBlocklistStrings) { + NSDictionary* originalHostInfo = [self parseHostString: subdomain]; + // nil means that we don't have anything valid to block in this entry + if (originalHostInfo == nil) continue; + + NSArray* relatedEntries = [self relatedBlockEntriesForEntry: originalHostInfo]; + NSArray* allEntries = [relatedEntries arrayByAddingObject: originalHostInfo]; + + for (NSDictionary* hostInfoDict in allEntries) { + [pf append] + } + } + // refresh pf rules so the new rules get read in + // [pfManager refreshPFRules]; +} + - (BOOL)clearBlock { [pf stopBlock: false]; BOOL pfSuccess = ![pf containsSelfControlBlock]; @@ -329,9 +334,44 @@ - (BOOL)domainIsGoogle:(NSString*)domainName { return [googleTester evaluateWithObject: domainName]; } +- (NSArray*)relatedBlockEntriesForEntry:(NSDictionary*)entryHostInfo { + // nil means that we don't have anything valid to block in this entry, therefore no related entries either + if (entryHostInfo == nil) return @[]; + + NSMutableArray* relatedEntries = [NSMutableArray array]; + + NSString* hostName = entryHostInfo[@"hostName"]; + + if (isAllowlist && includeLinkedDomains && ![hostName isValidIPAddress]) { + NSArray* relatedDomains = [[AllowlistScraper relatedDomains: hostName] allObjects]; + [relatedEntries addObjectsFromArray: relatedDomains]; + } + + if(![hostName isValidIPAddress] && includeCommonSubdomains) { + NSArray* commonSubdomains = [self commonSubdomainsForHostName: hostName]; + + for (NSString* subdomain in commonSubdomains) { + // we do not pull port, we leave the port number the same as we got it + NSDictionary* hostInfo = [self parseHostString: subdomain]; + + if (hostInfo == nil) continue; + + [relatedEntries addObject: hostInfo]; + } + } +} + - (NSDictionary*)parseHostString:(NSString*)hostString { - NSMutableDictionary* dict = [NSMutableDictionary dictionary]; + // don't do anything with blank hostnames, however they got on the list... + // otherwise they could end up screwing up the block + if (![[hostString stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] length]) { + return nil; + } + NSString* hostName; + // returning 0 for either of these values means "couldn't find it in the string" + int maskLen = 0; + int portNum = 0; NSArray* splitString = [hostString componentsSeparatedByString: @"/"]; hostName = splitString[0]; @@ -339,11 +379,7 @@ - (NSDictionary*)parseHostString:(NSString*)hostString { NSString* stringToSearchForPort = splitString[0]; if([splitString count] >= 2) { - int maskLen = [splitString[1] intValue]; - - if(maskLen != 0) { // 0 means we could not parse to int value - [dict setValue: @(maskLen) forKey: @"maskLen"]; - } + maskLen = [splitString[1] intValue]; // we expect the port number to come after the IP/masklen stringToSearchForPort = splitString[1]; @@ -357,20 +393,24 @@ - (NSDictionary*)parseHostString:(NSString*)hostString { } if([splitString count] >= 2) { - int portNum = [splitString[1] intValue]; - - if(portNum != 0) { // 0 means we could not parse to int value - [dict setValue: @(portNum) forKey: @"port"]; - } + portNum = [splitString[1] intValue]; } if([hostName isEqualToString: @""]) { hostName = @"*"; } - [dict setValue: hostName forKey: @"hostName"]; - - return dict; + // we won't block host * (everywhere) without a port number... it's just too likely to be mistaken. + // Use a allowlist if that's what you want! + if ([hostName isEqualToString: @"*"] && !portNum) { + return nil; + } + + return @{ + @"hostName": hostName, + @"port": @(portNum), + @"maskLen": @(maskLen) + }; } @end diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 40aff9a0..b1b4d266 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -16,122 +16,132 @@ @implementation SCDaemonBlockMethods + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { - NSLog(@"startign block in methods"); - if (blockIsRunningInSettingsOrDefaults(controllingUID)) { - NSLog(@"ERROR: Block is already running"); - NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -222 userInfo: @{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Block is already running", nil) - }]; - reply(err); - return; - } - - // clear any legacy block information - no longer useful since we're using SCSettings now - // (and could potentially confuse things) - SCSettings* settings = [SCSettings settingsForUser: controllingUID]; - [settings clearLegacySettings]; - - // update SCSettings with the blocklist and end date that've been requested - NSLog(@"Replacing settings end date %@ with %@, and blocklist %@ with %@ (%@ of %@)", [settings valueForKey: @"BlockEndDate"], endDate, [settings valueForKey: @"ActiveBlocklist"], blocklist, [blocklist class], [blocklist[0] class]); - [settings setValue: blocklist forKey: @"ActiveBlocklist"]; - [settings setValue: @(isAllowlist) forKey: @"ActiveBlockAsWhitelist"]; - [settings setValue: endDate forKey: @"BlockEndDate"]; - NSLog(@"And now ActiveBlocklist is %@", [settings valueForKey: @"ActiveBlocklist"]); - - if([blocklist count] <= 0 || ![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { - NSLog(@"ERROR: Blocklist is empty, or block end date is in the past"); - NSLog(@"Block End Date: %@ (%@), vs now is %@", [settings valueForKey: @"BlockEndDate"], [[settings valueForKey: @"BlockEndDate"] class], [NSDate date]); - NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -210 userInfo: @{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Blocklist is empty, or block end date is in the past", nil) - }]; - reply(err); - return; - } - - addRulesToFirewall(controllingUID); - [settings setValue: @YES forKey: @"BlockIsRunning"]; - [settings synchronizeSettings]; // synchronize ASAP since BlockIsRunning is a really important one - - // TODO: is this still necessary in the new daemon world? - sendConfigurationChangedNotification(); + @synchronized (self) { + NSLog(@"startign block in methods"); + if (blockIsRunningInSettingsOrDefaults(controllingUID)) { + NSLog(@"ERROR: Block is already running"); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -222 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Block is already running", nil) + }]; + reply(err); + return; + } + + // clear any legacy block information - no longer useful since we're using SCSettings now + // (and could potentially confuse things) + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + [settings clearLegacySettings]; + + // update SCSettings with the blocklist and end date that've been requested + NSLog(@"Replacing settings end date %@ with %@, and blocklist %@ with %@ (%@ of %@)", [settings valueForKey: @"BlockEndDate"], endDate, [settings valueForKey: @"ActiveBlocklist"], blocklist, [blocklist class], [blocklist[0] class]); + [settings setValue: blocklist forKey: @"ActiveBlocklist"]; + [settings setValue: @(isAllowlist) forKey: @"ActiveBlockAsWhitelist"]; + [settings setValue: endDate forKey: @"BlockEndDate"]; + NSLog(@"And now ActiveBlocklist is %@", [settings valueForKey: @"ActiveBlocklist"]); + + if([blocklist count] <= 0 || ![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { + NSLog(@"ERROR: Blocklist is empty, or block end date is in the past"); + NSLog(@"Block End Date: %@ (%@), vs now is %@", [settings valueForKey: @"BlockEndDate"], [[settings valueForKey: @"BlockEndDate"] class], [NSDate date]); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -210 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Blocklist is empty, or block end date is in the past", nil) + }]; + reply(err); + return; + } + + NSLog(@"Adding firewall rules..."); + addRulesToFirewall(controllingUID); + [settings setValue: @YES forKey: @"BlockIsRunning"]; + [settings synchronizeSettings]; // synchronize ASAP since BlockIsRunning is a really important one + + NSLog(@"Firewall rules added!"); + + // TODO: is this still necessary in the new daemon world? + sendConfigurationChangedNotification(); - // Clear all caches if the user has the correct preference set, so - // that blocked pages are not loaded from a cache. - clearCachesIfRequested(controllingUID); + // Clear all caches if the user has the correct preference set, so + // that blocked pages are not loaded from a cache. + clearCachesIfRequested(controllingUID); - NSLog(@"INFO: Block successfully added."); - reply(nil); + NSLog(@"INFO: Block successfully added."); + reply(nil); + } } + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { - SCSettings* settings = [SCSettings settingsForUser: controllingUID]; - - if(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]) { - // No block appears to be running at all in our settings. - // Most likely, the user removed it trying to get around the block. Boo! - // but for safety and to avoid permablocks (we no longer know when the block should end) - // we should clear the block now. - // but let them know that we noticed their (likely) cheating and we're not happy! - NSLog(@"INFO: Checkup ran, no active block found."); - [SCDaemonUtilities unloadDaemonJobForUID: controllingUID]; - - // get rid of this block - // Temporarily disabled the TamperingDetection flag because it was sometimes causing false positives - // (i.e. people having the background set repeatedly despite no attempts to cheat) - // We will try to bring this feature back once we can debug it - // GitHub issue: https://github.com/SelfControlApp/selfcontrol/issues/621 - // [settings setValue: @YES forKey: @"TamperingDetected"]; -// [settings synchronizeSettings]; -// -// removeBlock(controllingUID); - - // syncSettingsAndExit(settings, EX_SOFTWARE); - } - - if (![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { - NSLog(@"INFO: Checkup ran, block expired, removing block."); - - removeBlock(controllingUID); - [SCDaemonUtilities unloadDaemonJobForUID: controllingUID]; - - // Execution should never reach this point. Launchd unloading the job in - // should have killed this process. - printStatus(-216); - syncSettingsAndExit(settings, EX_SOFTWARE); - } else { - // The block is still on. Check if anybody removed our rules, and if so - // re-add them. Also make sure the user's settings are set to the correct - // settings just in case. - PacketFilter* pf = [[PacketFilter alloc] init]; - HostFileBlocker* hostFileBlocker = [[HostFileBlocker alloc] init]; - if(![pf containsSelfControlBlock] || (![[settings valueForKey: @"ActiveBlockAsWhitelist"] boolValue] && ![hostFileBlocker containsSelfControlBlock])) { - // The firewall is missing at least the block header. Let's clear everything - // before we re-add to make sure everything goes smoothly. - - [pf stopBlock: false]; - [hostFileBlocker writeNewFileContents]; - BOOL success = [hostFileBlocker writeNewFileContents]; - // Revert the host file blocker's file contents to disk so we can check - // whether or not it still contains the block (aka we messed up). - [hostFileBlocker revertFileContentsToDisk]; - if(!success || [hostFileBlocker containsSelfControlBlock]) { - NSLog(@"WARNING: Error removing host file block. Attempting to restore backup."); - - if([hostFileBlocker restoreBackupHostsFile]) - NSLog(@"INFO: Host file backup restored."); - else - NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); - } - - // Get rid of the backup file since we're about to make a new one. - [hostFileBlocker deleteBackupHostsFile]; - - // Perform the re-add of the rules - addRulesToFirewall(controllingUID); + NSLog(@"ChEcKup about to hit synchronized part"); + // TODO: is synchronized the wrong tool here? it could be several seconds for the block to start = a bunch of checkup threads piling up + @synchronized (self) { + NSLog(@"checkup synchronized starting!"); + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + + if(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]) { + // No block appears to be running at all in our settings. + // Most likely, the user removed it trying to get around the block. Boo! + // but for safety and to avoid permablocks (we no longer know when the block should end) + // we should clear the block now. + // but let them know that we noticed their (likely) cheating and we're not happy! + NSLog(@"INFO: Checkup ran, no active block found."); + [SCDaemonUtilities unloadDaemonJobForUID: controllingUID]; + + // get rid of this block + // Temporarily disabled the TamperingDetection flag because it was sometimes causing false positives + // (i.e. people having the background set repeatedly despite no attempts to cheat) + // We will try to bring this feature back once we can debug it + // GitHub issue: https://github.com/SelfControlApp/selfcontrol/issues/621 + // [settings setValue: @YES forKey: @"TamperingDetected"]; + // [settings synchronizeSettings]; + // + // removeBlock(controllingUID); + + // syncSettingsAndExit(settings, EX_SOFTWARE); + } + + if (![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { + NSLog(@"INFO: Checkup ran, block expired, removing block."); - clearCachesIfRequested(controllingUID); - NSLog(@"INFO: Checkup ran, readded block rules."); - } else NSLog(@"INFO: Checkup ran, no action needed."); + removeBlock(controllingUID); + [SCDaemonUtilities unloadDaemonJobForUID: controllingUID]; + + // Execution should never reach this point. Launchd unloading the job in + // should have killed this process. TODO: but maybe doesn't always with a daemon? + printStatus(-216); + syncSettingsAndExit(settings, EX_SOFTWARE); + } else { + // The block is still on. Check if anybody removed our rules, and if so + // re-add them. Also make sure the user's settings are set to the correct + // settings just in case. + PacketFilter* pf = [[PacketFilter alloc] init]; + HostFileBlocker* hostFileBlocker = [[HostFileBlocker alloc] init]; + if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlocker containsSelfControlBlock])) { + // The firewall is missing at least the block header. Let's clear everything + // before we re-add to make sure everything goes smoothly. + + [pf stopBlock: false]; + [hostFileBlocker writeNewFileContents]; + BOOL success = [hostFileBlocker writeNewFileContents]; + // Revert the host file blocker's file contents to disk so we can check + // whether or not it still contains the block (aka we messed up). + [hostFileBlocker revertFileContentsToDisk]; + if(!success || [hostFileBlocker containsSelfControlBlock]) { + NSLog(@"WARNING: Error removing host file block. Attempting to restore backup."); + + if([hostFileBlocker restoreBackupHostsFile]) + NSLog(@"INFO: Host file backup restored."); + else + NSLog(@"ERROR: Host file backup could not be restored. This may result in a permanent block."); + } + + // Get rid of the backup file since we're about to make a new one. + [hostFileBlocker deleteBackupHostsFile]; + + // Perform the re-add of the rules + addRulesToFirewall(controllingUID); + + clearCachesIfRequested(controllingUID); + NSLog(@"INFO: Checkup ran, readded block rules."); + } else NSLog(@"INFO: Checkup ran, no action needed."); + } } } diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m index daea8b7f..ae5764ce 100644 --- a/Daemon/SCDaemonUtilities.m +++ b/Daemon/SCDaemonUtilities.m @@ -8,6 +8,8 @@ #import "SCDaemonUtilities.h" #import #import "SCSettings.h" +#import "SCUtilities.h" +#import "BlockManager.h" @implementation SCDaemonUtilities @@ -42,4 +44,47 @@ + (void)unloadDaemonJobForUID:(uid_t)controllingUID { } } +// This method should be called when the blocklist has changed and a block is running +// to update the block to include newly added sites +// NOTE: this is a no-op for allowlist blocks (because we don't have the capability to do that), +// and when a block isn't running +// NOTE2: currently this works for _added_ sites, not removed ones, since that's all SC allows currently... ++ (void)updateActiveBlocklistForUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist { + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + + if(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]) { + NSLog(@"WARNING: Updating active blocklist but no block is running - ignoring."); + return; + } + + if ([settings boolForKey: @"ActiveBlockAsWhitelist"]) { + NSLog(@"WARNING: Updating active blocklist but this is not possible with an allowlist block - ignoring."); + } + + NSArray* activeBlocklist = [settings valueForKey: @"ActiveBlocklist"]; + NSMutableArray* added = [NSMutableArray arrayWithArray: newBlocklist]; + [added removeObjectsInArray: activeBlocklist]; + NSMutableArray* removed = [NSMutableArray arrayWithArray: activeBlocklist]; + [removed removeObjectsInArray: newBlocklist]; + + // throw a warning if something got removed for some reason, since we ignore them + if (removed.count > 0) { + NSLog(@"WARNING: Active blocklist has removed items; these will not be updated. Removed items are %@", removed); + } + + // TODO: add the added sites to the block via BlockManager + BlockManager* blockManager = [[BlockManager alloc] initAsAllowlist: [settings boolForKey: @"ActiveBlockAsWhitelist"] + allowLocal: [settings boolForKey: @"EvaluateCommonSubdomains"] + includeCommonSubdomains: [settings boolForKey: @"AllowLocalNetworks"] + includeLinkedDomains: [settings boolForKey: @"IncludeLinkedDomains"]]; + [blockManager enterAppendMode]; + + [blockManager prepareToAddBlock]; +// [blockManager addBlockEntries: [settings valueForKey: @"ActiveBlocklist"]]; + [blockManager finalizeBlock]; + + + [settings setValue: blocklist forKey: @"ActiveBlocklist"]; +} + @end diff --git a/Daemon/selfcontrold-Info.plist b/Daemon/selfcontrold-Info.plist index c5d33627..2ea8ea89 100755 --- a/Daemon/selfcontrold-Info.plist +++ b/Daemon/selfcontrold-Info.plist @@ -51,6 +51,11 @@ NSHumanReadableCopyright Free and open-source under the GPL. + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSMainNibFile MainMenu NSPrincipalClass diff --git a/HelperCommon.m b/HelperCommon.m index 4978808a..2d88553c 100644 --- a/HelperCommon.m +++ b/HelperCommon.m @@ -35,15 +35,17 @@ BOOL blockIsRunningInSettingsOrDefaults(uid_t controllingUID) { void addRulesToFirewall(uid_t controllingUID) { SCSettings* settings = [SCSettings settingsForUser: controllingUID]; - BOOL shouldEvaluateCommonSubdomains = [[settings valueForKey: @"EvaluateCommonSubdomains"] boolValue]; - BOOL allowLocalNetworks = [[settings valueForKey: @"AllowLocalNetworks"] boolValue]; - BOOL includeLinkedDomains = [[settings valueForKey: @"IncludeLinkedDomains"] boolValue]; + BOOL shouldEvaluateCommonSubdomains = [settings boolForKey: @"EvaluateCommonSubdomains"]; + BOOL allowLocalNetworks = [settings boolForKey: @"AllowLocalNetworks"]; + BOOL includeLinkedDomains = [settings boolForKey: @"IncludeLinkedDomains"]; // get value for ActiveBlockAsWhitelist - BOOL blockAsAllowlist = [[settings valueForKey: @"ActiveBlockAsWhitelist"] boolValue]; + BOOL blockAsAllowlist = [settings boolForKey: @"ActiveBlockAsWhitelist"]; BlockManager* blockManager = [[BlockManager alloc] initAsAllowlist: blockAsAllowlist allowLocal: allowLocalNetworks includeCommonSubdomains: shouldEvaluateCommonSubdomains includeLinkedDomains: includeLinkedDomains]; + NSLog(@"About to run BlockManager commands"); + [blockManager prepareToAddBlock]; [blockManager addBlockEntries: [settings valueForKey: @"ActiveBlocklist"]]; [blockManager finalizeBlock]; @@ -60,7 +62,7 @@ void removeRulesFromFirewall(uid_t controllingUID) { // notification) before we sleep to play the sound. Otherwise, // the app seems unresponsive and slow. SCSettings* settings = [SCSettings settingsForUser: controllingUID]; - if([[settings valueForKey: @"BlockSoundShouldPlay"] boolValue]) { + if([settings boolForKey: @"BlockSoundShouldPlay"]) { // Map the tags used in interface builder to the sound NSArray* systemSoundNames = [SCConstants systemSoundNames]; NSSound* alertSound = [NSSound soundNamed: systemSoundNames[[[settings valueForKey: @"BlockSound"] intValue]]]; @@ -120,7 +122,7 @@ void removeRulesFromFirewall(uid_t controllingUID) { void clearCachesIfRequested(uid_t controllingUID) { SCSettings* settings = [SCSettings settingsForUser: controllingUID]; - if(![[settings valueForKey: @"ClearCaches"] boolValue]) { + if(![settings boolForKey: @"ClearCaches"]) { return; } diff --git a/HelperMain.m b/HelperMain.m index 1e2d424b..12817b2f 100755 --- a/HelperMain.m +++ b/HelperMain.m @@ -271,7 +271,7 @@ int main(int argc, char* argv[]) { // settings just in case. PacketFilter* pf = [[PacketFilter alloc] init]; HostFileBlocker* hostFileBlocker = [[HostFileBlocker alloc] init]; - if(![pf containsSelfControlBlock] || (![[settings valueForKey: @"ActiveBlockAsWhitelist"] boolValue] && ![hostFileBlocker containsSelfControlBlock])) { + if(![pf containsSelfControlBlock] || (![settings boolForKey: @"ActiveBlockAsWhitelist"] && ![hostFileBlocker containsSelfControlBlock])) { // The firewall is missing at least the block header. Let's clear everything // before we re-add to make sure everything goes smoothly. diff --git a/PacketFilter.h b/PacketFilter.h index 8d731029..7ef9cd8d 100644 --- a/PacketFilter.h +++ b/PacketFilter.h @@ -22,5 +22,6 @@ - (int)stopBlock:(BOOL)force; - (void)addSelfControlConfig; - (BOOL)containsSelfControlBlock; +- (int)refreshPFRules; @end diff --git a/PacketFilter.m b/PacketFilter.m index 1bbd3db5..3eb5b2ab 100644 --- a/PacketFilter.m +++ b/PacketFilter.m @@ -47,32 +47,42 @@ - (void)addAllowlistFooter:(NSMutableString*)configText { [configText appendString: @"pass out proto tcp from any to any port 68\n"]; } +- (NSArray*)ruleStringsForIP:(NSString*)ip port:(int)port maskLen:(int)maskLen { + NSMutableString* rule = [NSMutableString stringWithString: @"from any to "]; + + if (ip) { + [rule appendString: ip]; + } else { + [rule appendString: @"any"]; + } + + if (maskLen) { + [rule appendString: [NSString stringWithFormat: @"/%d", maskLen]]; + } + + if (port) { + [rule appendString: [NSString stringWithFormat: @" port %d", port]]; + } + + if (isAllowlist) { + return @[ + [NSString stringWithFormat: @"pass out proto tcp %@\n", rule], + [NSString stringWithFormat: @"pass out proto udp %@\n", rule] + ]; + } else { + return @[ + [NSString stringWithFormat: @"block return out proto tcp %@\n", rule], + [NSString stringWithFormat: @"block return out proto udp %@\n", rule] + ]; + } +} - (void)addRuleWithIP:(NSString*)ip port:(int)port maskLen:(int)maskLen { - NSMutableString* rule = [NSMutableString stringWithString: @"from any to "]; - - if (ip) { - [rule appendString: ip]; - } else { - [rule appendString: @"any"]; - } - - if (maskLen) { - [rule appendString: [NSString stringWithFormat: @"/%d", maskLen]]; - } - - if (port) { - [rule appendString: [NSString stringWithFormat: @" port %d", port]]; - } - - @synchronized(self) { - if (isAllowlist) { - [rules appendString: [NSString stringWithFormat: @"pass out proto tcp %@\n", rule]]; - [rules appendString: [NSString stringWithFormat: @"pass out proto udp %@\n", rule]]; - } else { - [rules appendString: [NSString stringWithFormat: @"block return out proto tcp %@\n", rule]]; - [rules appendString: [NSString stringWithFormat: @"block return out proto udp %@\n", rule]]; - } - } + @synchronized(self) { + NSArray* ruleStrings = [self ruleStringsForIP: ip port: port maskLen: maskLen]; + for (NSString* ruleString in ruleStrings) { + [rules appendString: ruleString]; + } + } } - (void)writeConfiguration { @@ -88,6 +98,34 @@ - (void)writeConfiguration { [filterConfiguration writeToFile: @"/etc/pf.anchors/org.eyebeam" atomically: true encoding: NSUTF8StringEncoding error: nil]; } +- (void)appendRulesToCurrentBlockConfiguration:(NSArray*)newEntryDicts { + if (newEntryDicts.count < 1) return; + if (isAllowlist) { + NSLog(@"WARNING: Can't append rules to allowlist blocks - ignoring"); + return; + } + + // open the file and prepare to write to the very bottom (no footer since it's not an allowlist) + NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingAtPath: @"/etc/pf.anchors/org.eyebeam"]; + if (!fileHandle) { + NSLog(@"ERROR: Failed to get handle for pf.anchors file while attempting to append rules"); + return; + } + + [fileHandle seekToEndOfFile]; + for (NSDictionary* entryHostInfo in newEntryDicts) { + NSString* hostName = hostInfo[@"hostName"]; + int portNum = [hostInfo[@"port"] intValue]; + int maskLen = [hostInfo[@"maskLen"] intValue]; + + NSArray* ruleStrings = [self ruleStringsForIP: hostName port: port maskLen: maskLen]; + for (NSString* ruleString in ruleStrings) { + [fileHandle writeData: [ruleString dataUsingEncoding:NSUTF8StringEncoding]]; + } + } + [fileHandle closeFile]; +} + - (int)startBlock { [self addSelfControlConfig]; [self writeConfiguration]; @@ -118,6 +156,17 @@ - (int)startBlock { return [task terminationStatus]; } +- (int)refreshPFRules { + NSArray* args = [@"-f /etc/pf.conf -F states" componentsSeparatedByString: @" "]; + + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath: kPfctlExecutablePath]; + [task setArguments: args]; + [task launch]; + [task waitUntilExit]; + + return [task terminationStatus]; +} - (void)writePFToken:(NSString*)token error:(NSError**)error { [token writeToFile: @"/etc/SelfControlPFToken" atomically: YES encoding: NSUTF8StringEncoding error: error]; diff --git a/PreferencesAdvancedViewController.m b/PreferencesAdvancedViewController.m index 24d3acc8..579952f7 100644 --- a/PreferencesAdvancedViewController.m +++ b/PreferencesAdvancedViewController.m @@ -22,10 +22,10 @@ - (instancetype)init { - (void)refreshFromSecuredSettings { SCSettings* settings = [SCSettings currentUserSettings]; - BOOL clearCaches = [[settings valueForKey: @"ClearCaches"] boolValue]; - BOOL allowLocalNetworks = [[settings valueForKey: @"AllowLocalNetworks"] boolValue]; - BOOL evaluateCommonSubdomains = [[settings valueForKey: @"EvaluateCommonSubdomains"] boolValue]; - BOOL includeLinkedDomains = [[settings valueForKey: @"IncludeLinkedDomains"] boolValue]; + BOOL clearCaches = [settings boolForKey: @"ClearCaches"]; + BOOL allowLocalNetworks = [settings boolForKey: @"AllowLocalNetworks"]; + BOOL evaluateCommonSubdomains = [settings boolForKey: @"EvaluateCommonSubdomains"]; + BOOL includeLinkedDomains = [settings boolForKey: @"IncludeLinkedDomains"]; self.clearCachesCheckbox.state = clearCaches; self.allowLocalCheckbox.state = allowLocalNetworks; diff --git a/PreferencesGeneralViewController.m b/PreferencesGeneralViewController.m index d67e4c91..9a652825 100644 --- a/PreferencesGeneralViewController.m +++ b/PreferencesGeneralViewController.m @@ -22,8 +22,8 @@ - (instancetype)init { - (void)refreshBlockSoundFromSettings { SCSettings* settings = [SCSettings currentUserSettings]; - BOOL blockSoundShouldPlay = [[settings valueForKey: @"BlockSoundShouldPlay"] boolValue]; - NSInteger blockSoundIndex = [[settings valueForKey: @"BlockSound"] integerValue]; + BOOL blockSoundShouldPlay = [settings boolForKey: @"BlockSoundShouldPlay"]; + NSInteger blockSoundIndex = [[settings boolForKey: @"BlockSound"] integerValue]; self.playSoundCheckbox.state = blockSoundShouldPlay; [self.soundMenu selectItemAtIndex: blockSoundIndex]; diff --git a/SCAppXPC.m b/SCAppXPC.m index 48186454..dbd74123 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -120,7 +120,7 @@ - (void)getVersion { } - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { - NSLog(@"sending command block"); + NSLog(@"sending install command block"); [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Install command failed with connection error: %@", connectError); diff --git a/SCSettings.h b/SCSettings.h index b7c435f2..6f574dca 100644 --- a/SCSettings.h +++ b/SCSettings.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setValue:(id)value forKey:(NSString*)key stopPropagation:(BOOL)stopPropagation; - (void)setValue:(nullable id)value forKey:(NSString*)key; - (id)valueForKey:(NSString*)key; +- (BOOL)boolForKey:(NSString*)key - (void)migrateLegacySettings; - (void)clearLegacySettings; diff --git a/SCSettings.m b/SCSettings.m index a4d317b0..77b7a08a 100644 --- a/SCSettings.m +++ b/SCSettings.m @@ -389,6 +389,9 @@ - (id)valueForKey:(NSString*)key { return value; } +- (BOOL)boolForKey:(NSString*)key { + return [[self valueForKey: key] boolValue]; +} // We might have "legacy" block settings hiding in one of two places: // - a "lock file" at /etc/SelfControl.lock (aka SelfControlLegacyLockFilePath) diff --git a/SCUtilities.h b/SCUtilities.h index 7d416ec0..0da623da 100644 --- a/SCUtilities.h +++ b/SCUtilities.h @@ -42,7 +42,7 @@ + (NSDate*) endDateFromLegacyBlockDictionary:(NSDictionary *)dict; // read and write saved block files -+ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL settings:(SCSettings*)settings errorDescription:(NSString**)errDescriptionRef; ++ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; + (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL; @end diff --git a/SCUtilities.m b/SCUtilities.m index 2f0da1d2..ab7d9662 100644 --- a/SCUtilities.m +++ b/SCUtilities.m @@ -202,10 +202,9 @@ + (NSDate*) endDateFromLegacyBlockDictionary:(NSDictionary *)dict { return [startDate dateByAddingTimeInterval: (duration * 60)]; } -+ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL settings:(SCSettings*)settings errorDescription:(NSString**)errDescriptionRef { - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; - NSDictionary* saveDict = @{@"HostBlacklist": [defaults arrayForKey: @"Blocklist"], - @"BlockAsWhitelist": [defaults objectForKey: @"BlockAsWhitelist"]}; ++ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef { + NSDictionary* saveDict = @{@"HostBlacklist": [blockInfo objectForKey: @"Blocklist"], + @"BlockAsWhitelist": [blockInfo objectForKey: @"BlockAsWhitelist"]}; NSString* saveDataErr; NSData* saveData = [NSPropertyListSerialization dataFromPropertyList: saveDict format: NSPropertyListBinaryFormat_v1_0 errorDescription: &saveDataErr]; From 77c2fbcec288c84ee4144e875faafc0acbee0498 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 3 Jan 2021 14:31:32 -0800 Subject: [PATCH 11/72] add-to-blocklist mostly works --- AppController.h | 4 +- AppController.m | 135 +++++------------------------ BlockManager.h | 1 + BlockManager.m | 46 +++++----- Daemon/SCDaemonBlockMethods.h | 2 + Daemon/SCDaemonBlockMethods.m | 60 +++++++++++++ Daemon/SCDaemonProtocol.h | 2 + Daemon/SCDaemonUtilities.m | 9 +- Daemon/SCDaemonXPC.m | 8 +- HostFileBlocker.h | 1 + HostFileBlocker.m | 33 ++++++- PacketFilter.h | 2 + PacketFilter.m | 38 ++++++-- PreferencesGeneralViewController.m | 2 +- SCAppXPC.h | 1 + SCAppXPC.m | 17 ++++ SCSettings.h | 2 +- 17 files changed, 212 insertions(+), 151 deletions(-) diff --git a/AppController.h b/AppController.h index 0eba7b55..2532dd75 100755 --- a/AppController.h +++ b/AppController.h @@ -110,7 +110,7 @@ // Called by timerWindowController_ after its sheet returns, to add a specified // host to the blocklist (and refresh the block to use the new blocklist). Launches -// a new thread with refreshBlock: +// a new thread with addToBlocklist: - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock; // Called by timerWindowController_ after its sheet returns, to add a specified @@ -129,7 +129,7 @@ // Gets authorization for and then immediately refreshes the block by calling // SelfControl's helper tool with the appropriate arguments. Meant to be called // as a separate thread. -- (void)refreshBlock:(NSLock*)lockToUse; +- (void)updateActiveBlocklist:(NSLock*)lockToUse; // open preferences panel - (IBAction)openPreferences:(id)sender; diff --git a/AppController.m b/AppController.m index c64a0292..94a662e8 100755 --- a/AppController.m +++ b/AppController.m @@ -455,6 +455,7 @@ - (BOOL)networkConnectionIsAvailable { } - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { + NSLog(@"addToBlocklist: %@", host); NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; NSArray* cleanedEntries = [SCUtilities cleanBlocklistEntry: host]; @@ -465,7 +466,7 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { [list addObject: entry]; } - [settings_ setValue: list forKey: @"ActiveBlocklist"]; + [defaults_ setValue: list forKey: @"Blocklist"]; if(![self blockIsRunning]) { // This method shouldn't be getting called, a block is not on. @@ -474,11 +475,6 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { // before we return. [self refreshUserInterface]; - // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; - [list removeLastObject]; - [settings_ setValue: list forKey: @"ActiveBlocklist"]; - NSError* err = [NSError errorWithDomain:kSelfControlErrorDomain code: -103 userInfo: @{NSLocalizedDescriptionKey: @"Error -103: Attempting to add host to block, but no block appears to be in progress."}]; @@ -496,11 +492,6 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { [networkUnavailableAlert addButtonWithTitle: NSLocalizedString(@"Network Diagnostics...", @"Network Diagnostics button")]; if([networkUnavailableAlert runModal] == NSAlertFirstButtonReturn) { // User clicked cancel - // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; - [list removeLastObject]; - [settings_ setValue: list forKey: @"ActiveBlocklist"]; - return; } @@ -510,15 +501,10 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { CFNetDiagnosticRef diagRef = CFNetDiagnosticCreateWithURL(kCFAllocatorDefault, url); CFNetDiagnosticDiagnoseProblemInteractively(diagRef); - // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; - [list removeLastObject]; - [settings_ setValue: list forKey: @"ActiveBlocklist"]; - return; } - [NSThread detachNewThreadSelector: @selector(refreshBlock:) toTarget: self withObject: lock]; + [NSThread detachNewThreadSelector: @selector(updateActiveBlocklist:) toTarget: self withObject: lock]; } - (void)extendBlockTime:(NSInteger)minutesToAdd lock:(NSLock*)lock { @@ -773,107 +759,30 @@ - (void)installBlock { } } -- (void)refreshBlock:(NSLock*)lockToUse { +- (void)updateActiveBlocklist:(NSLock*)lockToUse { + NSLog(@"updateActiveBlocklist"); if(![lockToUse tryLock]) { return; } - @autoreleasepool { - AuthorizationRef authorizationRef; - char* helperToolPath = [self selfControlHelperToolPathUTF8String]; - long helperToolPathSize = strlen(helperToolPath); - AuthorizationItem right = { - kAuthorizationRightExecute, - helperToolPathSize, - helperToolPath, - 0 - }; - AuthorizationRights authRights = { - 1, - &right - }; - AuthorizationFlags myFlags = kAuthorizationFlagDefaults | - kAuthorizationFlagExtendRights | - kAuthorizationFlagInteractionAllowed; - OSStatus status; - - status = AuthorizationCreate (&authRights, - kAuthorizationEmptyEnvironment, - myFlags, - &authorizationRef); - - if(status) { - NSLog(@"ERROR: Failed to authorize block refresh."); - - // Reverse the blocklist change made before we fail - NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; - [list removeLastObject]; - [settings_ setValue: list forKey: @"ActiveBlocklist"]; - - [lockToUse unlock]; - - return; - } - - // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid - [settings_ synchronizeSettings]; - - // We need to pass our UID to the helper tool. It needs to know whose defaults - // it should read in order to properly load the blocklist. - char uidString[32]; - snprintf(uidString, sizeof(uidString), "%d", getuid()); - - FILE* commPipe; - - char* args[] = { uidString, "--refresh", NULL }; - status = AuthorizationExecuteWithPrivileges(authorizationRef, - helperToolPath, - kAuthorizationFlagDefaults, - args, - &commPipe); - - if(status) { - NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %d", (int)status); - - NSError* err = [self errorFromHelperToolStatusCode: status]; - - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: err - waitUntilDone: YES]; - - [lockToUse unlock]; - - return; - } - - NSFileHandle* helperToolHandle = [[NSFileHandle alloc] initWithFileDescriptor: fileno(commPipe) closeOnDealloc: YES]; - - NSData* inData = [helperToolHandle readDataToEndOfFile]; - NSString* inDataString = [[NSString alloc] initWithData: inData encoding: NSUTF8StringEncoding]; - - if([inDataString isEqualToString: @""]) { - NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain - code: -105 - userInfo: @{NSLocalizedDescriptionKey: @"Error -105: The helper tool crashed. This may cause unexpected errors."}]; - - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: err - waitUntilDone: YES]; - } - - int exitCode = [inDataString intValue]; - - if(exitCode) { - NSError* err = [self errorFromHelperToolStatusCode: exitCode]; - - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: err - waitUntilDone: YES]; - } + // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid + [settings_ synchronizeSettings]; - [timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; - } - [lockToUse unlock]; + // ok, the new helper tool is installed! refresh the connection, then it's time to start the block + [self.xpc refreshConnectionAndRun:^{ + NSLog(@"Refreshed connection updating active blocklist!"); + // [self.xpc getVersion]; + [self.xpc updateBlocklistWithControllingUID: 501 // TODO: don't hardcode the user ID + newBlocklist: [self->defaults_ arrayForKey: @"Blocklist"] + authorization: [NSData new] + reply:^(NSError * _Nonnull error) { + NSLog(@"WOO updated block with error %@", error); + + [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; + + [lockToUse unlock]; + }]; + }]; } // it really sucks, but we can't change any values that are KVO-bound to the UI unless they're on the main thread diff --git a/BlockManager.h b/BlockManager.h index 052309cb..1c12940f 100644 --- a/BlockManager.h +++ b/BlockManager.h @@ -42,6 +42,7 @@ - (BlockManager*)initAsAllowlist:(BOOL)allowlist allowLocal:(BOOL)local includeCommonSubdomains:(BOOL)blockCommon includeLinkedDomains:(BOOL)includeLinked; - (void)enterAppendMode; +- (void)finishAppending; - (void)prepareToAddBlock; - (void)finalizeBlock; - (void)addBlockEntryFromString:(NSString*)entry; diff --git a/BlockManager.m b/BlockManager.m index 93e65ca9..a302bf43 100644 --- a/BlockManager.m +++ b/BlockManager.m @@ -25,6 +25,8 @@ @implementation BlockManager +BOOL appendMode = NO; + - (BlockManager*)init { return [self initAsAllowlist: NO allowLocal: YES includeCommonSubdomains: YES]; } @@ -73,11 +75,27 @@ - (void)prepareToAddBlock { } - (void)enterAppendMode { + if (isAllowlist) { + NSLog(@"ERROR: can't append to allowlist block"); + return; + } if(![hostsBlocker containsSelfControlBlock]) { NSLog(@"ERROR: can't append to hosts block that doesn't yet exist"); return; } - [hostsBlocker enterAppendMode]; + + appendMode = YES; + [pf enterAppendMode]; +} +- (void)finishAppending { + NSLog(@"About to run operation queue for appending..."); + [opQueue waitUntilAllOperationsAreFinished]; + NSLog(@"Operation queue ran!"); + + [hostsBlocker writeNewFileContents]; + [pf finishAppending]; + [pf refreshPFRules]; + appendMode = NO; } - (void)finalizeBlock { @@ -121,7 +139,11 @@ - (void)addBlockEntryWithHostName:(NSString*)hostName port:(int)portNum maskLen: } if(hostsBlockingEnabled && ![hostName isEqualToString: @"*"] && !portNum && !isIP) { - [hostsBlocker addRuleBlockingDomain: hostName]; + if (appendMode) { + [hostsBlocker appendExistingBlockWithRuleForDomain: hostName]; + } else { + [hostsBlocker addRuleBlockingDomain: hostName]; + } } } @@ -156,24 +178,6 @@ - (void)addBlockEntries:(NSArray*)blockList { } } -- (void)appendBlockEntriesToRunningBlock:(NSArray*)addedBlocklistStrings { - /// no operations queue here, presumably we didn't add too much so it's OK to just do it in one loop - for (NSString* addedBlockString in addedBlocklistStrings) { - NSDictionary* originalHostInfo = [self parseHostString: subdomain]; - // nil means that we don't have anything valid to block in this entry - if (originalHostInfo == nil) continue; - - NSArray* relatedEntries = [self relatedBlockEntriesForEntry: originalHostInfo]; - NSArray* allEntries = [relatedEntries arrayByAddingObject: originalHostInfo]; - - for (NSDictionary* hostInfoDict in allEntries) { - [pf append] - } - } - // refresh pf rules so the new rules get read in - // [pfManager refreshPFRules]; -} - - (BOOL)clearBlock { [pf stopBlock: false]; BOOL pfSuccess = ![pf containsSelfControlBlock]; @@ -359,6 +363,8 @@ - (BOOL)domainIsGoogle:(NSString*)domainName { [relatedEntries addObject: hostInfo]; } } + + return relatedEntries; } - (NSDictionary*)parseHostString:(NSString*)hostString { diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index d5ac1d77..c9663326 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)checkupBlockWithControllingUID:(uid_t)controllingUID; ++ (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + @end NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index b1b4d266..e7f2fda7 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -10,6 +10,7 @@ #import "HelperCommon.h" #import "PacketFilter.h" #import "SCDaemonUtilities.h" +#import "BlockManager.h" NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; @@ -68,6 +69,65 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + @synchronized (self) { + NSLog(@"updating blocklist in methods"); + if (!blockIsRunningInSettingsOrDefaults(controllingUID)) { + NSLog(@"ERROR: Can't update blocklist since block isn't running"); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -213 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Refreshing blocklist, but no block is currently running", nil) + }]; + reply(err); + return; + } + + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + + if ([settings boolForKey: @"ActiveBlockAsWhitelist"]) { + NSLog(@"ERROR: Attempting to update active blocklist, but this is not possible with an allowlist block"); + // TODO: replace this with a better error code + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -213 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Attempting to update active blocklist, but this is not possible with an allowlist block", nil) + }]; + reply(err); + return; + } + + NSArray* activeBlocklist = [settings valueForKey: @"ActiveBlocklist"]; + NSMutableArray* added = [NSMutableArray arrayWithArray: newBlocklist]; + [added removeObjectsInArray: activeBlocklist]; + NSMutableArray* removed = [NSMutableArray arrayWithArray: activeBlocklist]; + [removed removeObjectsInArray: newBlocklist]; + + // throw a warning if something got removed for some reason, since we ignore them + if (removed.count > 0) { + NSLog(@"WARNING: Active blocklist has removed items; these will not be updated. Removed items are %@", removed); + } + + BlockManager* blockManager = [[BlockManager alloc] initAsAllowlist: [settings boolForKey: @"ActiveBlockAsWhitelist"] + allowLocal: [settings boolForKey: @"EvaluateCommonSubdomains"] + includeCommonSubdomains: [settings boolForKey: @"AllowLocalNetworks"] + includeLinkedDomains: [settings boolForKey: @"IncludeLinkedDomains"]]; + [blockManager enterAppendMode]; + NSLog(@"adding block entries for %@ (diffed new arr %@ from old %@)", added, newBlocklist, activeBlocklist); + [blockManager addBlockEntries: added]; + [blockManager finishAppending]; + + [settings setValue: newBlocklist forKey: @"ActiveBlocklist"]; + [settings synchronizeSettings]; // make sure everyone knows about our new list + + // TODO: is this still necessary in the new daemon world? + sendConfigurationChangedNotification(); + + // Clear all caches if the user has the correct preference set, so + // that blocked pages are not loaded from a cache. + clearCachesIfRequested(controllingUID); + + NSLog(@"INFO: Blocklist successfully updated."); + reply(nil); + } +} + + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { NSLog(@"ChEcKup about to hit synchronized part"); // TODO: is synchronized the wrong tool here? it could be several seconds for the block to start = a bunch of checkup threads piling up diff --git a/Daemon/SCDaemonProtocol.h b/Daemon/SCDaemonProtocol.h index d47d86e9..fb5a58d8 100644 --- a/Daemon/SCDaemonProtocol.h +++ b/Daemon/SCDaemonProtocol.h @@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + - (BOOL) checkup; - (void)getVersionWithReply:(void(^)(NSString * version))reply; diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m index ae5764ce..733015dd 100644 --- a/Daemon/SCDaemonUtilities.m +++ b/Daemon/SCDaemonUtilities.m @@ -78,13 +78,10 @@ + (void)updateActiveBlocklistForUID:(uid_t)controllingUID newBlocklist:(NSArray< includeCommonSubdomains: [settings boolForKey: @"AllowLocalNetworks"] includeLinkedDomains: [settings boolForKey: @"IncludeLinkedDomains"]]; [blockManager enterAppendMode]; - - [blockManager prepareToAddBlock]; -// [blockManager addBlockEntries: [settings valueForKey: @"ActiveBlocklist"]]; - [blockManager finalizeBlock]; + [blockManager addBlockEntries: added]; + [blockManager finishAppending]; - - [settings setValue: blocklist forKey: @"ActiveBlocklist"]; + [settings setValue: newBlocklist forKey: @"ActiveBlocklist"]; } @end diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 15a0d1c1..a2d641ca 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -16,11 +16,17 @@ @implementation SCDaemonXPC // TODO: make this run without dependence on the user or even settings - should just pass the blocklist right to the method - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { - NSLog(@"XPC method called: startBlockWithReply"); + NSLog(@"XPC method called: startBlockWithControllingUID"); [SCDaemonBlockMethods startBlockWithControllingUID: controllingUID blocklist: blocklist isAllowlist:isAllowlist endDate: endDate authorization: authData reply: reply]; } +- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"XPC method called: updateBlocklistWithControllingUID"); + + [SCDaemonBlockMethods updateBlocklist: controllingUID newBlocklist: newBlocklist authorization: authData reply: reply]; +} + - (BOOL) checkup { NSLog(@"XPC method called: checkup"); return YES; diff --git a/HostFileBlocker.h b/HostFileBlocker.h index fb5b51c8..11c39129 100755 --- a/HostFileBlocker.h +++ b/HostFileBlocker.h @@ -45,6 +45,7 @@ - (BOOL)restoreBackupHostsFile; - (void)addRuleBlockingDomain:(NSString*)domainName; +- (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName; - (BOOL)containsSelfControlBlock; diff --git a/HostFileBlocker.m b/HostFileBlocker.m index 4c1da630..c16b24bd 100755 --- a/HostFileBlocker.m +++ b/HostFileBlocker.m @@ -118,13 +118,42 @@ - (void)addSelfControlBlockFooter { [strLock unlock]; } +- (NSArray*)ruleStringsToBlockDomain:(NSString*)domainName { + return @[ + [NSString stringWithFormat: @"0.0.0.0\t%@\n", domainName], + [NSString stringWithFormat: @"::\t%@\n", domainName] + ]; +} + - (void)addRuleBlockingDomain:(NSString*)domainName { [strLock lock]; - [newFileContents appendString: [NSString stringWithFormat: @"0.0.0.0\t%@\n", domainName]]; - [newFileContents appendString: [NSString stringWithFormat: @"::\t%@\n", domainName]]; + NSArray* ruleStrings = [self ruleStringsToBlockDomain: domainName]; + for (NSString* ruleString in ruleStrings) { + [newFileContents appendString: ruleString]; + } [strLock unlock]; } +- (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName { + [strLock lock]; + NSRange footerLocation = [newFileContents rangeOfString: kHostFileBlockerSelfControlFooter]; + if (footerLocation.location == NSNotFound) { + // we can't append if a block isn't in the file already! + NSLog(@"WARNING: can't append to host block because footer can't be found"); + } else { + // combine the rule strings and insert em all at once to make the math easier + NSArray* ruleStrings = [self ruleStringsToBlockDomain: domainName]; + + NSMutableString* combinedRuleString = [NSMutableString string]; + for (NSString* ruleString in ruleStrings) { + [combinedRuleString appendString: ruleString]; + } + + [newFileContents insertString: combinedRuleString atIndex: footerLocation.location]; + } + [strLock unlock]; +} + - (BOOL)containsSelfControlBlock { [strLock lock]; diff --git a/PacketFilter.h b/PacketFilter.h index 7ef9cd8d..d05bec10 100644 --- a/PacketFilter.h +++ b/PacketFilter.h @@ -22,6 +22,8 @@ - (int)stopBlock:(BOOL)force; - (void)addSelfControlConfig; - (BOOL)containsSelfControlBlock; +- (void)enterAppendMode; +- (void)finishAppending; - (int)refreshPFRules; @end diff --git a/PacketFilter.m b/PacketFilter.m index 3eb5b2ab..f69b3740 100644 --- a/PacketFilter.m +++ b/PacketFilter.m @@ -12,6 +12,8 @@ @implementation PacketFilter +NSFileHandle* appendFileHandle; + - (PacketFilter*)initAsAllowlist: (BOOL)allowlist { if (self = [super init]) { isAllowlist = allowlist; @@ -80,7 +82,11 @@ - (void)addRuleWithIP:(NSString*)ip port:(int)port maskLen:(int)maskLen { @synchronized(self) { NSArray* ruleStrings = [self ruleStringsForIP: ip port: port maskLen: maskLen]; for (NSString* ruleString in ruleStrings) { - [rules appendString: ruleString]; + if (appendFileHandle) { + [appendFileHandle writeData: [ruleString dataUsingEncoding:NSUTF8StringEncoding]]; + } else { + [rules appendString: ruleString]; + } } } } @@ -98,6 +104,26 @@ - (void)writeConfiguration { [filterConfiguration writeToFile: @"/etc/pf.anchors/org.eyebeam" atomically: true encoding: NSUTF8StringEncoding error: nil]; } +- (void)enterAppendMode { + if (isAllowlist) { + NSLog(@"WARNING: Can't append rules to allowlist blocks - ignoring"); + return; + } + + // open the file and prepare to write to the very bottom (no footer since it's not an allowlist) + appendFileHandle = [NSFileHandle fileHandleForWritingAtPath: @"/etc/pf.anchors/org.eyebeam"]; + if (!appendFileHandle) { + NSLog(@"ERROR: Failed to get handle for pf.anchors file while attempting to append rules"); + return; + } + + [appendFileHandle seekToEndOfFile]; +} +- (void)finishAppending { + [appendFileHandle closeFile]; + appendFileHandle = nil; +} + - (void)appendRulesToCurrentBlockConfiguration:(NSArray*)newEntryDicts { if (newEntryDicts.count < 1) return; if (isAllowlist) { @@ -106,6 +132,8 @@ - (void)appendRulesToCurrentBlockConfiguration:(NSArray*)newEntry } // open the file and prepare to write to the very bottom (no footer since it's not an allowlist) + // NOTE FOR FUTURE: NSFileHandle can't append lines to the middle of the file anyway, + // would need to read in the whole thing + write out again NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingAtPath: @"/etc/pf.anchors/org.eyebeam"]; if (!fileHandle) { NSLog(@"ERROR: Failed to get handle for pf.anchors file while attempting to append rules"); @@ -114,11 +142,11 @@ - (void)appendRulesToCurrentBlockConfiguration:(NSArray*)newEntry [fileHandle seekToEndOfFile]; for (NSDictionary* entryHostInfo in newEntryDicts) { - NSString* hostName = hostInfo[@"hostName"]; - int portNum = [hostInfo[@"port"] intValue]; - int maskLen = [hostInfo[@"maskLen"] intValue]; + NSString* hostName = entryHostInfo[@"hostName"]; + int portNum = [entryHostInfo[@"port"] intValue]; + int maskLen = [entryHostInfo[@"maskLen"] intValue]; - NSArray* ruleStrings = [self ruleStringsForIP: hostName port: port maskLen: maskLen]; + NSArray* ruleStrings = [self ruleStringsForIP: hostName port: portNum maskLen: maskLen]; for (NSString* ruleString in ruleStrings) { [fileHandle writeData: [ruleString dataUsingEncoding:NSUTF8StringEncoding]]; } diff --git a/PreferencesGeneralViewController.m b/PreferencesGeneralViewController.m index 9a652825..cdc20582 100644 --- a/PreferencesGeneralViewController.m +++ b/PreferencesGeneralViewController.m @@ -23,7 +23,7 @@ - (instancetype)init { - (void)refreshBlockSoundFromSettings { SCSettings* settings = [SCSettings currentUserSettings]; BOOL blockSoundShouldPlay = [settings boolForKey: @"BlockSoundShouldPlay"]; - NSInteger blockSoundIndex = [[settings boolForKey: @"BlockSound"] integerValue]; + NSInteger blockSoundIndex = [[settings valueForKey: @"BlockSound"] integerValue]; self.playSoundCheckbox.state = blockSoundShouldPlay; [self.soundMenu selectItemAtIndex: blockSoundIndex]; diff --git a/SCAppXPC.h b/SCAppXPC.h index 9e562c73..4d71bb30 100644 --- a/SCAppXPC.h +++ b/SCAppXPC.h @@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)getVersion; - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; @end diff --git a/SCAppXPC.m b/SCAppXPC.m index dbd74123..b198a9ce 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -137,4 +137,21 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + [self connectAndExecuteCommandBlock:^(NSError * connectError) { + if (connectError != nil) { + NSLog(@"Blocklist update failed with connection error: %@", connectError); + reply(connectError); + } else { + [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { + NSLog(@"Blocklist update command failed with remote object proxy error: %@", proxyError); + reply(proxyError); + }] updateBlocklistWithControllingUID: controllingUID newBlocklist: newBlocklist authorization: [NSData new] reply:^(NSError* error) { + NSLog(@"Blocklist update failed with error = %@\n", error); + reply(error); + }]; + } + }]; +} + @end diff --git a/SCSettings.h b/SCSettings.h index 6f574dca..2ef90d94 100644 --- a/SCSettings.h +++ b/SCSettings.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setValue:(id)value forKey:(NSString*)key stopPropagation:(BOOL)stopPropagation; - (void)setValue:(nullable id)value forKey:(NSString*)key; - (id)valueForKey:(NSString*)key; -- (BOOL)boolForKey:(NSString*)key +- (BOOL)boolForKey:(NSString*)key; - (void)migrateLegacySettings; - (void)clearLegacySettings; From 11d77a5d1f65ca54f3416c65da5676466d3fd20b Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 3 Jan 2021 14:49:15 -0800 Subject: [PATCH 12/72] Add to blocklist REALLY works --- BlockManager.m | 11 ++++++++--- HostFileBlocker.m | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/BlockManager.m b/BlockManager.m index a302bf43..512f073c 100644 --- a/BlockManager.m +++ b/BlockManager.m @@ -84,6 +84,7 @@ - (void)enterAppendMode { return; } + hostsBlockingEnabled = YES; appendMode = YES; [pf enterAppendMode]; } @@ -112,6 +113,7 @@ - (void)finalizeBlock { } - (void)enqueueBlockEntryWithHostName:(NSString*)hostName port:(int)portNum maskLen:(int)maskLen { + NSLog(@"enqueueBlockEntryWithHostName %@", hostName); NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{ [self addBlockEntryWithHostName: hostName port: portNum maskLen: maskLen]; }]; @@ -139,6 +141,7 @@ - (void)addBlockEntryWithHostName:(NSString*)hostName port:(int)portNum maskLen: } if(hostsBlockingEnabled && ![hostName isEqualToString: @"*"] && !portNum && !isIP) { + NSLog(@"About to add rule for %@, append mode is %d", hostName, appendMode); if (appendMode) { [hostsBlocker appendExistingBlockWithRuleForDomain: hostName]; } else { @@ -160,16 +163,18 @@ - (void)addBlockEntryFromString:(NSString*)entry { [self addBlockEntryWithHostName: hostName port: portNum maskLen: maskLen]; NSArray* relatedEntries = [self relatedBlockEntriesForEntry: hostInfo]; + NSLog(@"Enqueuing related entries to %@: %@", hostName, relatedEntries); for (NSDictionary* relatedHostInfo in relatedEntries) { - NSString* relatedHostName = hostInfo[@"hostName"]; - int relatedPortNum = [hostInfo[@"port"] intValue]; - int relatedMaskLen = [hostInfo[@"maskLen"] intValue]; + NSString* relatedHostName = relatedHostInfo[@"hostName"]; + int relatedPortNum = [relatedHostInfo[@"port"] intValue]; + int relatedMaskLen = [relatedHostInfo[@"maskLen"] intValue]; [self enqueueBlockEntryWithHostName: relatedHostName port: relatedPortNum maskLen: relatedMaskLen]; } } - (void)addBlockEntries:(NSArray*)blockList { + NSLog(@"addBlockEntries %@", blockList); for(int i = 0; i < [blockList count]; i++) { NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{ [self addBlockEntryFromString: blockList[i]]; diff --git a/HostFileBlocker.m b/HostFileBlocker.m index c16b24bd..248d4edf 100755 --- a/HostFileBlocker.m +++ b/HostFileBlocker.m @@ -127,6 +127,7 @@ - (void)addSelfControlBlockFooter { - (void)addRuleBlockingDomain:(NSString*)domainName { [strLock lock]; + NSLog(@"host file blocker: add rule to block domain %@", domainName); NSArray* ruleStrings = [self ruleStringsToBlockDomain: domainName]; for (NSString* ruleString in ruleStrings) { [newFileContents appendString: ruleString]; @@ -136,6 +137,7 @@ - (void)addRuleBlockingDomain:(NSString*)domainName { - (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName { [strLock lock]; + NSLog(@"host file blocker: append rule to block domain %@", domainName); NSRange footerLocation = [newFileContents rangeOfString: kHostFileBlockerSelfControlFooter]; if (footerLocation.location == NSNotFound) { // we can't append if a block isn't in the file already! @@ -149,6 +151,8 @@ - (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName { [combinedRuleString appendString: ruleString]; } + NSLog(@"inserting combined rule string %@ at %d", combinedRuleString, footerLocation.location); + [newFileContents insertString: combinedRuleString atIndex: footerLocation.location]; } [strLock unlock]; From 47a7d28edda9ce8c73f3c3f8f8ad12cb2a6435ad Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 3 Jan 2021 16:59:53 -0800 Subject: [PATCH 13/72] lil comment tweaks --- AppController.h | 2 +- AppController.m | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AppController.h b/AppController.h index 2532dd75..c1ac806c 100755 --- a/AppController.h +++ b/AppController.h @@ -114,7 +114,7 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock; // Called by timerWindowController_ after its sheet returns, to add a specified -// number of minutes to the black timer. Launches a new thread with refreshBlock. +// number of minutes to the black timer. - (void)extendBlockTime:(NSInteger)minutes lock:(NSLock*)lock; // Converts a failure exit code from a helper tool invocation into an NSError, diff --git a/AppController.m b/AppController.m index 94a662e8..da73e12d 100755 --- a/AppController.m +++ b/AppController.m @@ -815,6 +815,8 @@ - (void)extendBlockDuration:(NSDictionary*)options { // synchronize it to disk to the helper tool knows immediately [settings_ synchronizeSettings]; + // TODO: send configuration changed notification so the helper tool knows faster? + // let the timer know it needs to recalculate [timerWindowController_ performSelectorOnMainThread:@selector(blockEndDateUpdated) withObject: nil From 7d609b99797ec558587b11f570b1311f2233dd14 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 3 Jan 2021 18:25:16 -0800 Subject: [PATCH 14/72] XPC connection improvements --- AppController.h | 8 -- AppController.m | 203 ++++++++-------------------------- Daemon/SCDaemonBlockMethods.m | 3 +- SCAppXPC.h | 1 + SCAppXPC.m | 105 +++++++++++++++++- SCConstants.h | 2 + SCConstants.m | 2 + 7 files changed, 155 insertions(+), 169 deletions(-) diff --git a/AppController.h b/AppController.h index c1ac806c..94117f43 100755 --- a/AppController.h +++ b/AppController.h @@ -56,14 +56,6 @@ @property (assign) BOOL addingBlock; -// Returns an autoreleased instance of the path to the helper tool inside -// SelfControl's bundle -@property (nonatomic, readonly, copy) NSString *selfControlHelperToolPath; - -// Returns as a UTF-8 encoded C-string the path to the helper tool inside -// SelfControl's bundle -- (char*)selfControlHelperToolPathUTF8String; - // Called when the block duration slider is moved. Updates the label that gives // the block duration in words (hours and minutes). - (IBAction)updateTimeSliderDisplay:(id)sender; diff --git a/AppController.m b/AppController.m index da73e12d..95996a03 100755 --- a/AppController.m +++ b/AppController.m @@ -31,8 +31,7 @@ #import "SCSettings.h" #import #import "SCAppXPC.h" - -NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; +#import "SCConstants.h" @interface AppController () {} @@ -77,32 +76,6 @@ - (AppController*) init { return self; } -- (NSString*)selfControlHelperToolPath { - static NSString* path; - - // Cache the path so it doesn't have to be searched for again. - if(!path) { - NSBundle* thisBundle = [NSBundle mainBundle]; - path = [thisBundle.bundlePath stringByAppendingString: @"/Contents/Library/LaunchServices/org.eyebeam.selfcontrold"]; - } - - return path; -} - -- (char*)selfControlHelperToolPathUTF8String { - static char* path; - - // Cache the converted path so it doesn't have to be converted again - if(!path) { - path = malloc(512); - [[self selfControlHelperToolPath] getCString: path - maxLength: 512 - encoding: NSUTF8StringEncoding]; - } - - return path; -} - - (IBAction)updateTimeSliderDisplay:(id)sender { NSInteger numMinutes = [defaults_ integerForKey: @"BlockDuration"]; @@ -247,16 +220,14 @@ - (void)refreshUserInterface { [self updateTimeSliderDisplay: blockDurationSlider_]; - BOOL addBlockIsOngoing = self.addingBlock; - - if([defaults_ integerForKey: @"BlockDuration"] != 0 && [[defaults_ arrayForKey: @"Blocklist"] count] != 0 && !addBlockIsOngoing) { + if([defaults_ integerForKey: @"BlockDuration"] != 0 && [[defaults_ arrayForKey: @"Blocklist"] count] != 0 && !self.addingBlock) { [submitButton_ setEnabled: YES]; } else { [submitButton_ setEnabled: NO]; } // If we're adding a block, we want buttons disabled. - if(!addBlockIsOngoing) { + if(!self.addingBlock) { [blockDurationSlider_ setEnabled: YES]; [editBlocklistButton_ setEnabled: YES]; [submitButton_ setTitle: NSLocalizedString(@"Start", @"Start button")]; @@ -408,9 +379,8 @@ - (BOOL)blockIsRunning { return [SCUtilities blockIsRunningWithSettings: settings_ defaults: defaults_]; } -- (IBAction)showDomainList:(id)sender { - BOOL addBlockIsOngoing = self.addingBlock; - if([self blockIsRunning] || addBlockIsOngoing) { +- (IBAction)showDomainList:(id)sender { + if([self blockIsRunning] || self.addingBlock) { NSAlert* blockInProgressAlert = [[NSAlert alloc] init]; [blockInProgressAlert setMessageText: NSLocalizedString(@"Block in progress", @"Block in progress error title")]; [blockInProgressAlert setInformativeText:NSLocalizedString(@"The blocklist cannot be edited while a block is in progress.", @"Block in progress explanation")]; @@ -632,130 +602,44 @@ - (void)installBlock { @autoreleasepool { self.addingBlock = true; [self refreshUserInterface]; - AuthorizationRef authorizationRef; - NSLog(@"helper tool path is %@", [self selfControlHelperToolPath]); - char* helperToolPath = [self selfControlHelperToolPathUTF8String]; - NSUInteger helperToolPathSize = strlen(helperToolPath); - AuthorizationItem right = { - kSMRightBlessPrivilegedHelper, - helperToolPathSize, - helperToolPath, - 0 - }; - AuthorizationRights authRights = { - 1, - &right - }; - AuthorizationFlags myFlags = kAuthorizationFlagDefaults | - kAuthorizationFlagExtendRights | - kAuthorizationFlagInteractionAllowed; - OSStatus status; - - status = AuthorizationCreate (&authRights, - kAuthorizationEmptyEnvironment, - myFlags, - &authorizationRef); - - if(status) { - NSLog(@"ERROR: Failed to authorize block start."); - self.addingBlock = false; - [self refreshUserInterface]; - return; - } - - // for legacy reasons, BlockDuration is in minutes, so convert it to seconds before passing it through] - NSTimeInterval blockDurationSecs = [[defaults_ valueForKey: @"BlockDuration"] intValue] * 60; - [SCUtilities startBlockInSettings: settings_ withBlockDuration: blockDurationSecs]; - - // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid - [settings_ synchronizeSettings]; - - CFErrorRef cfError; - BOOL result = (BOOL)SMJobBless( - kSMDomainSystemLaunchd, - CFSTR("org.eyebeam.selfcontrold"), - authorizationRef, - &cfError); - - - if(!result) { - NSError* error = CFBridgingRelease(cfError); - - NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %d and error %@", (int)status, error); - - // reset settings on failure, and record that on disk ASAP - [SCUtilities removeBlockFromSettings: settings_]; - [settings_ synchronizeSettings]; - - NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain - code: status - userInfo: @{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"Error %d received from the Security Server.", (int)status]}]; - - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: err - waitUntilDone: YES]; - - self.addingBlock = false; - [self refreshUserInterface]; - - return; - } else NSLog(@"succeeded in installing helper tool"); - - - // ok, the new helper tool is installed! refresh the connection, then it's time to start the block - [self.xpc refreshConnectionAndRun:^{ - NSLog(@"Refreshed connection!"); - // [self.xpc getVersion]; - [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID - blocklist: [self->defaults_ arrayForKey: @"Blocklist"] - isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"] - endDate: [self->settings_ valueForKey: @"BlockEndDate"] - authorization: [NSData new] - reply:^(NSError * _Nonnull error) { - NSLog(@"WOO started block with error %@", error); - + [self.xpc installHelperTool:^(NSError * _Nonnull error) { + if (error != nil) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; self.addingBlock = false; [self refreshUserInterface]; - }]; -// [self.xpc getVersion]; + return; + } else { + // helper tool installed successfully, let's prepare to start the block! + // for legacy reasons, BlockDuration is in minutes, so convert it to seconds before passing it through] + NSTimeInterval blockDurationSecs = [[self->defaults_ valueForKey: @"BlockDuration"] intValue] * 60; + [SCUtilities startBlockInSettings: self->settings_ withBlockDuration: blockDurationSecs]; + + // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid + [self->settings_ synchronizeSettings]; + + // ok, the new helper tool is installed! refresh the connection, then it's time to start the block + [self.xpc refreshConnectionAndRun:^{ + NSLog(@"Refreshed connection and ready to start block!"); + [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID + blocklist: [self->defaults_ arrayForKey: @"Blocklist"] + isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"] + endDate: [self->settings_ valueForKey: @"BlockEndDate"] + authorization: [NSData new] + reply:^(NSError * _Nonnull error) { + NSLog(@"WOO started block with error %@", error); + if (error != nil) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } + self.addingBlock = false; + [self refreshUserInterface]; + }]; + }]; + } }]; -// NSFileHandle* helperToolHandle = [[NSFileHandle alloc] initWithFileDescriptor: fileno(commPipe) closeOnDealloc: YES]; -// -// NSData* inData = [helperToolHandle readDataToEndOfFile]; -// -// -// NSString* inDataString = [[NSString alloc] initWithData: inData encoding: NSUTF8StringEncoding]; -// -// if([inDataString isEqualToString: @""]) { -// // reset settings on failure, and record that on disk ASAP -// [SCUtilities removeBlockFromSettings: settings_]; -// [settings_ synchronizeSettings]; -// -// NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain -// code: -104 -// userInfo: @{NSLocalizedDescriptionKey: @"Error -104: The helper tool crashed. This may cause unexpected errors."}]; -// -// [NSApp performSelectorOnMainThread: @selector(presentError:) -// withObject: err -// waitUntilDone: YES]; -// } -// -// int exitCode = [inDataString intValue]; -// -// if(exitCode) { -// // reset settings on failure, and record that on disk ASAP -// [SCUtilities removeBlockFromSettings: settings_]; -// [settings_ synchronizeSettings]; -// -// NSError* err = [self errorFromHelperToolStatusCode: exitCode]; -// -// [NSApp performSelectorOnMainThread: @selector(presentError:) -// withObject: err -// waitUntilDone: YES]; -// } -// -// self.addingBlock = false; -// [self refreshUserInterface]; } } @@ -771,7 +655,6 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { // ok, the new helper tool is installed! refresh the connection, then it's time to start the block [self.xpc refreshConnectionAndRun:^{ NSLog(@"Refreshed connection updating active blocklist!"); - // [self.xpc getVersion]; [self.xpc updateBlocklistWithControllingUID: 501 // TODO: don't hardcode the user ID newBlocklist: [self->defaults_ arrayForKey: @"Blocklist"] authorization: [NSData new] @@ -780,6 +663,12 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; + if (error != nil) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } + [lockToUse unlock]; }]; }]; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index e7f2fda7..4beec762 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -11,8 +11,7 @@ #import "PacketFilter.h" #import "SCDaemonUtilities.h" #import "BlockManager.h" - -NSString* const kSelfControlErrorDomain = @"SelfControlErrorDomain"; +#import "SCConstants.h" @implementation SCDaemonBlockMethods diff --git a/SCAppXPC.h b/SCAppXPC.h index 4d71bb30..54eba955 100644 --- a/SCAppXPC.h +++ b/SCAppXPC.h @@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SCAppXPC : NSObject - (void)connectToHelperTool; +- (void)installHelperTool:(void(^)(NSError*))callback; - (void)refreshConnectionAndRun:(void(^)(void))callback; - (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock; diff --git a/SCAppXPC.m b/SCAppXPC.m index b198a9ce..7e2f7830 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -7,6 +7,8 @@ #import "SCAppXPC.h" #import "SCDaemonProtocol.h" +#import +#import "SCConstants.h" @interface SCAppXPC () {} @@ -38,8 +40,8 @@ - (void)connectToHelperTool { connection.invalidationHandler = ^{ // If the connection gets invalidated then, on the main thread, nil out our // reference to it. This ensures that we attempt to rebuild it the next time around. - connection.invalidationHandler = nil; - NSLog(@"called invalidation handler"); + connection.invalidationHandler = connection.interruptionHandler = nil; + NSLog(@"called invalidation handler, self.daemonConnection is %@", self.daemonConnection); if (connection == self.daemonConnection) { // dispatch_sync on main thread would deadlock, so be careful @@ -57,12 +59,84 @@ - (void)connectToHelperTool { NSLog(@"connection invalidated"); } else NSLog(@"not invalidating connection cause they don't match"); }; + // our interruption handler is just our invalidation handler, except we retry afterward + connection.interruptionHandler = ^{ + NSLog(@"Helper tool connection interrupted"); + connection.invalidationHandler(); + + // interruptions may have happened because the daemon crashed + // so wait a second and try to reconnect + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + NSLog(@"Retrying helper tool connection!"); + [self connectToHelperTool]; + }); + }; + #pragma clang diagnostic pop [self.daemonConnection resume]; NSLog(@"Started helper connection!"); } } + +- (void)installHelperTool:(void(^)(NSError*))callback { + AuthorizationRef authorizationRef; + NSLog(@"helper tool path is %@", [self selfControlHelperToolPath]); + char* helperToolPath = [self selfControlHelperToolPathUTF8String]; + NSUInteger helperToolPathSize = strlen(helperToolPath); + AuthorizationItem right = { + kSMRightBlessPrivilegedHelper, + helperToolPathSize, + helperToolPath, + 0 + }; + AuthorizationRights authRights = { + 1, + &right + }; + AuthorizationFlags myFlags = kAuthorizationFlagDefaults | + kAuthorizationFlagExtendRights | + kAuthorizationFlagInteractionAllowed; + OSStatus status; + + status = AuthorizationCreate (&authRights, + kAuthorizationEmptyEnvironment, + myFlags, + &authorizationRef); + + if(status) { + NSLog(@"ERROR: Failed to authorize installing selfcontrold."); + callback([NSError errorWithDomain: @"SelfControlErrorDomain" code: status userInfo: nil]); + return; + } + + CFErrorRef cfError; + BOOL result = (BOOL)SMJobBless( + kSMDomainSystemLaunchd, + CFSTR("org.eyebeam.selfcontrold"), + authorizationRef, + &cfError); + + if(!result) { + NSError* error = CFBridgingRelease(cfError); + + NSLog(@"WARNING: Authorized installation of selfcontrold returned failure status code %d and error %@", (int)status, error); + + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain + code: status + userInfo: @{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"Error %d received from the Security Server.", (int)status]}]; + callback(err); + return; + } else { + NSLog(@"succeeded in installing helper tool"); + callback(nil); + } +} + +- (BOOL)connectionIsActive { + return (self.daemonConnection != nil); +} + - (void)refreshConnectionAndRun:(void(^)(void))callback { // when we're refreshing the connection, we can end up in a slightly awkward situation: // if we call invalidate, but immediately start to reconnect before daemonConnection can be nil'd out @@ -83,6 +157,7 @@ - (void)refreshConnectionAndRun:(void(^)(void))callback { callback(); }; + NSLog(@"About to invalidate connection on main thread"); [self.daemonConnection performSelectorOnMainThread: @selector(invalidate) withObject: nil waitUntilDone: YES]; } @@ -154,4 +229,30 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS }]; } +- (NSString*)selfControlHelperToolPath { + static NSString* path; + + // Cache the path so it doesn't have to be searched for again. + if(!path) { + NSBundle* thisBundle = [NSBundle mainBundle]; + path = [thisBundle.bundlePath stringByAppendingString: @"/Contents/Library/LaunchServices/org.eyebeam.selfcontrold"]; + } + + return path; +} + +- (char*)selfControlHelperToolPathUTF8String { + static char* path; + + // Cache the converted path so it doesn't have to be converted again + if(!path) { + path = malloc(512); + [[self selfControlHelperToolPath] getCString: path + maxLength: 512 + encoding: NSUTF8StringEncoding]; + } + + return path; +} + @end diff --git a/SCConstants.h b/SCConstants.h index 47c1027d..e083e82c 100644 --- a/SCConstants.h +++ b/SCConstants.h @@ -9,6 +9,8 @@ NS_ASSUME_NONNULL_BEGIN +FOUNDATION_EXPORT NSString *const kSelfControlErrorDomain; + @interface SCConstants : NSObject + (NSArray*) systemSoundNames; diff --git a/SCConstants.m b/SCConstants.m index f9d1f09c..65ae7f97 100644 --- a/SCConstants.m +++ b/SCConstants.m @@ -7,6 +7,8 @@ #import "SCConstants.h" +NSString *const kSelfControlErrorDomain = @"SelfControlErrorDomain"; + @implementation SCConstants + (NSArray*)systemSoundNames { From eaaccebe9ee3a70d28c88ff82103fa4bdd9e42cb Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 3 Jan 2021 18:27:44 -0800 Subject: [PATCH 15/72] Use real user IDs instead of hardcoded ones --- AppController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppController.m b/AppController.m index 95996a03..c169c495 100755 --- a/AppController.m +++ b/AppController.m @@ -622,7 +622,7 @@ - (void)installBlock { // ok, the new helper tool is installed! refresh the connection, then it's time to start the block [self.xpc refreshConnectionAndRun:^{ NSLog(@"Refreshed connection and ready to start block!"); - [self.xpc startBlockWithControllingUID: 501 // TODO: don't hardcode the user ID + [self.xpc startBlockWithControllingUID: getuid() blocklist: [self->defaults_ arrayForKey: @"Blocklist"] isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"] endDate: [self->settings_ valueForKey: @"BlockEndDate"] @@ -655,7 +655,7 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { // ok, the new helper tool is installed! refresh the connection, then it's time to start the block [self.xpc refreshConnectionAndRun:^{ NSLog(@"Refreshed connection updating active blocklist!"); - [self.xpc updateBlocklistWithControllingUID: 501 // TODO: don't hardcode the user ID + [self.xpc updateBlocklistWithControllingUID: getuid() newBlocklist: [self->defaults_ arrayForKey: @"Blocklist"] authorization: [NSData new] reply:^(NSError * _Nonnull error) { From fe6eaa579116828d6e97e6b435d6d7f8a1202348 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 4 Jan 2021 00:04:48 -0800 Subject: [PATCH 16/72] Only accept daemon connections from SC.app --- Daemon/SCDaemon.m | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 2c1a48bc..18f86d38 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -38,6 +38,23 @@ - (void)start { #pragma mark - NSXPCListenerDelegate - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { + // There is a potential security issue / race condition with matching based on PID, but seems unlikely in this case + NSDictionary* guestAttributes = @{ + (id)kSecGuestAttributePid: @(newConnection.processIdentifier) + }; + SecCodeRef guest; + SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(guestAttributes), kSecCSDefaultFlags, &guest); + SecRequirementRef isSelfControlApp; + // TODO: should this check for a specific certificate? currently only verifies that it's an Apple-signed app with identifier org.eyebeam.SelfControl + SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\""), kSecCSDefaultFlags, &isSelfControlApp); + OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp); + + if (clientValidityStatus) { + NSError* error = [NSError errorWithDomain: NSOSStatusErrorDomain code: clientValidityStatus userInfo: nil]; + NSLog(@"Rejecting XPC connection because of invalid client signing. Error was %@", error); + return NO; + } + SCDaemonXPC* scdXPC = [[SCDaemonXPC alloc] init]; newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol: @protocol(SCDaemonProtocol)]; newConnection.exportedObject = scdXPC; From 8afb46e1d20655077c5a106a9ad04c4d5213cecd Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 4 Jan 2021 17:44:57 -0800 Subject: [PATCH 17/72] Require a properly signed certificate to talk to the daemon --- Daemon/SCDaemon.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 18f86d38..e53f79ff 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -45,8 +45,7 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne SecCodeRef guest; SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(guestAttributes), kSecCSDefaultFlags, &guest); SecRequirementRef isSelfControlApp; - // TODO: should this check for a specific certificate? currently only verifies that it's an Apple-signed app with identifier org.eyebeam.SelfControl - SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\""), kSecCSDefaultFlags, &isSelfControlApp); + SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\" and certificate leaf[subject.OU] = L6W5L88KN7"), kSecCSDefaultFlags, &isSelfControlApp); OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp); if (clientValidityStatus) { From 3111304f7b51536e2b835cbf16093a2c47070824 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 4 Jan 2021 18:57:19 -0800 Subject: [PATCH 18/72] more xpc auth hardening --- Daemon/SCDaemon.m | 14 +- Daemon/SCDaemonXPC.m | 8 ++ SCAppXPC.m | 40 +++++- SCXPCAuthorization.h | 24 ++++ SCXPCAuthorization.m | 179 ++++++++++++++++++++++++++ SelfControl.xcodeproj/project.pbxproj | 10 ++ 6 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 SCXPCAuthorization.h create mode 100644 SCXPCAuthorization.m diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index e53f79ff..c52acb6d 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -12,6 +12,13 @@ static NSString* serviceName = @"org.eyebeam.selfcontrold"; +@interface NSXPCConnection(PrivateAuditToken) + +// This property exists, but it's private. Make it available: +@property (nonatomic, readonly) audit_token_t auditToken; + +@end + @interface SCDaemon () @property (nonatomic, strong, readwrite) NSXPCListener* listener; @@ -38,14 +45,15 @@ - (void)start { #pragma mark - NSXPCListenerDelegate - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { - // There is a potential security issue / race condition with matching based on PID, but seems unlikely in this case + // There is a potential security issue / race condition with matching based on PID, so we use the (technically private) auditToken instead + audit_token_t auditToken = newConnection.auditToken; NSDictionary* guestAttributes = @{ - (id)kSecGuestAttributePid: @(newConnection.processIdentifier) + (id)kSecGuestAttributeAudit: [NSData dataWithBytes: &auditToken length: sizeof(audit_token_t)] }; SecCodeRef guest; SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(guestAttributes), kSecCSDefaultFlags, &guest); SecRequirementRef isSelfControlApp; - SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\" and certificate leaf[subject.OU] = L6W5L88KN7"), kSecCSDefaultFlags, &isSelfControlApp); + SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7)"), kSecCSDefaultFlags, &isSelfControlApp); OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp); if (clientValidityStatus) { diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index a2d641ca..4e313cd2 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -8,6 +8,7 @@ #import "SCDaemonXPC.h" #import "version-header.h" #import "SCDaemonBlockMethods.h" +#import "SCXPCAuthorization.h" @implementation SCDaemonXPC @@ -18,6 +19,13 @@ @implementation SCDaemonXPC - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { NSLog(@"XPC method called: startBlockWithControllingUID"); + NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; + if (error != nil) { + NSLog(@"ERROR: XPC authorization failed due to error %@", error); + reply(error); + return; + } + [SCDaemonBlockMethods startBlockWithControllingUID: controllingUID blocklist: blocklist isAllowlist:isAllowlist endDate: endDate authorization: authData reply: reply]; } diff --git a/SCAppXPC.m b/SCAppXPC.m index 7e2f7830..28fc0845 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -9,21 +9,57 @@ #import "SCDaemonProtocol.h" #import #import "SCConstants.h" +#import "SCXPCAuthorization.h" -@interface SCAppXPC () {} +@interface SCAppXPC () { + AuthorizationRef _authRef; +} @property (atomic, strong, readwrite) NSXPCConnection* daemonConnection; +@property (atomic, copy, readwrite) NSData* authorization; @end @implementation SCAppXPC +- (void)setupAuthorization { + // this all mostly copied from Apple's Even Better Authorization Sample + OSStatus err; + AuthorizationExternalForm extForm; + + // Create our connection to the authorization system. + // + // If we can't create an authorization reference then the app is not going to be able + // to do anything requiring authorization. Generally this only happens when you launch + // the app in some wacky, and typically unsupported, way. In the debug build we flag that + // with an assert. In the release build we continue with self->_authRef as NULL, which will + // cause all authorized operations to fail. + + err = AuthorizationCreate(NULL, NULL, 0, &self->_authRef); + if (err == errAuthorizationSuccess) { + err = AuthorizationMakeExternalForm(self->_authRef, &extForm); + self.authorization = [[NSData alloc] initWithBytes: &extForm length: sizeof(extForm)]; + } + assert(err == errAuthorizationSuccess); + + // If we successfully connected to Authorization Services, add definitions for our default + // rights (unless they're already in the database). + + if (self->_authRef) { + [SCXPCAuthorization setupAuthorizationRights: self->_authRef]; + } + +} + // Ensures that we're connected to our helper tool // should only be called from the main thread // Copied from Apple's EvenBetterAuthorizationSample - (void)connectToHelperTool { assert([NSThread isMainThread]); NSLog(@"Connecting to helper tool, daemon connection is %@", self.daemonConnection); + + [self setupAuthorization]; + if (self.daemonConnection == nil) { self.daemonConnection = [[NSXPCConnection alloc] initWithMachServiceName: @"org.eyebeam.selfcontrold" options: NSXPCConnectionPrivileged]; self.daemonConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SCDaemonProtocol)]; @@ -204,7 +240,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray + +NS_ASSUME_NONNULL_BEGIN + +@interface SCXPCAuthorization : NSObject + ++ (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command; + ++ (NSString *)authorizationRightForCommand:(SEL)command; + // For a given command selector, return the associated authorization right name. + ++ (void)setupAuthorizationRights:(AuthorizationRef)authRef; + // Set up the default authorization rights in the authorization database. + +@end + +NS_ASSUME_NONNULL_END diff --git a/SCXPCAuthorization.m b/SCXPCAuthorization.m new file mode 100644 index 00000000..4591fafc --- /dev/null +++ b/SCXPCAuthorization.m @@ -0,0 +1,179 @@ +// +// SCXPCAuthorization.m +// SelfControl +// +// Created by Charlie Stigler on 1/4/21. +// + +#import "SCXPCAuthorization.h" + +@implementation SCXPCAuthorization + +// all of these methods (basically this whole file) copied from Apple's Even Better Authorization Sample code + +static NSString * kCommandKeyAuthRightName = @"authRightName"; +static NSString * kCommandKeyAuthRightDefault = @"authRightDefault"; +static NSString * kCommandKeyAuthRightDesc = @"authRightDescription"; + +// copied from Apple's Even Better Authorization Sample code ++ (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command + // Check that the client denoted by authData is allowed to run the specified command. + // authData is expected to be an NSData with an AuthorizationExternalForm embedded inside. +{ + #pragma unused(authData) + NSError * error; + OSStatus err; + OSStatus junk; + AuthorizationRef authRef; + + assert(command != nil); + + authRef = NULL; + + // First check that authData looks reasonable. + + error = nil; + if ( (authData == nil) || ([authData length] != sizeof(AuthorizationExternalForm)) ) { + error = [NSError errorWithDomain:NSOSStatusErrorDomain code:paramErr userInfo:nil]; + } + + // Create an authorization ref from that the external form data contained within. + + if (error == nil) { + err = AuthorizationCreateFromExternalForm([authData bytes], &authRef); + + // Authorize the right associated with the command. + + if (err == errAuthorizationSuccess) { + AuthorizationItem oneRight = { NULL, 0, NULL, 0 }; + AuthorizationRights rights = { 1, &oneRight }; + + oneRight.name = [[SCXPCAuthorization authorizationRightForCommand:command] UTF8String]; + assert(oneRight.name != NULL); + + err = AuthorizationCopyRights( + authRef, + &rights, + NULL, + kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed, + NULL + ); + } + if (err != errAuthorizationSuccess) { + error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]; + } + } + + if (authRef != NULL) { + junk = AuthorizationFree(authRef, 0); + assert(junk == errAuthorizationSuccess); + } + + return error; +} + + ++ (NSDictionary *)commandInfo +{ + static dispatch_once_t sOnceToken; + static NSDictionary * sCommandInfo; + + dispatch_once(&sOnceToken, ^{ + sCommandInfo = @{ + NSStringFromSelector(@selector(startBlockWithControllingUID:blocklist:isAllowlist:endDate:authorization:reply:)) : @{ + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.startBlock", + kCommandKeyAuthRightDefault : @kAuthorizationRuleAuthenticateAsAdmin, + kCommandKeyAuthRightDesc : NSLocalizedString( + @"SelfControl needs your username and password to start the block.", + @"prompt shown when user is required to authorize to read the license key" + ) + }, + NSStringFromSelector(@selector(writeLicenseKey:authorization:withReply:)) : @{ + kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.writeLicenseKey", + kCommandKeyAuthRightDefault : @kAuthorizationRuleAuthenticateAsAdmin, + kCommandKeyAuthRightDesc : NSLocalizedString( + @"EBAS is trying to write its license key.", + @"prompt shown when user is required to authorize to write the license key" + ) + }, + NSStringFromSelector(@selector(bindToLowNumberPortAuthorization:withReply:)) : @{ + kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.startWebService", + kCommandKeyAuthRightDefault : @kAuthorizationRuleClassAllow, + kCommandKeyAuthRightDesc : NSLocalizedString( + @"EBAS is trying to start its web service.", + @"prompt shown when user is required to authorize to start the web service" + ) + } + }; + }); + return sCommandInfo; +} + ++ (void)enumerateRightsUsingBlock:(void (^)(NSString * authRightName, id authRightDefault, NSString * authRightDesc))block + // Calls the supplied block with information about each known authorization right.. +{ + [self.commandInfo enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + #pragma unused(key) + #pragma unused(stop) + NSDictionary * commandDict; + NSString * authRightName; + id authRightDefault; + NSString * authRightDesc; + + // If any of the following asserts fire it's likely that you've got a bug + // in sCommandInfo. + + commandDict = (NSDictionary *) obj; + assert([commandDict isKindOfClass:[NSDictionary class]]); + + authRightName = [commandDict objectForKey:kCommandKeyAuthRightName]; + assert([authRightName isKindOfClass:[NSString class]]); + + authRightDefault = [commandDict objectForKey:kCommandKeyAuthRightDefault]; + assert(authRightDefault != nil); + + authRightDesc = [commandDict objectForKey:kCommandKeyAuthRightDesc]; + assert([authRightDesc isKindOfClass:[NSString class]]); + + block(authRightName, authRightDefault, authRightDesc); + }]; +} + ++ (void)setupAuthorizationRights:(AuthorizationRef)authRef + // See comment in header. +{ + assert(authRef != NULL); + [SCXPCAuthorization enumerateRightsUsingBlock:^(NSString * authRightName, id authRightDefault, NSString * authRightDesc) { + OSStatus blockErr; + + // First get the right. If we get back errAuthorizationDenied that means there's + // no current definition, so we add our default one. + + blockErr = AuthorizationRightGet([authRightName UTF8String], NULL); + if (blockErr == errAuthorizationDenied) { + blockErr = AuthorizationRightSet( + authRef, // authRef + [authRightName UTF8String], // rightName + (__bridge CFTypeRef) authRightDefault, // rightDefinition + (__bridge CFStringRef) authRightDesc, // descriptionKey + NULL, // bundle (NULL implies main bundle) + CFSTR("SCXPCAuthorization") // localeTableName + ); + assert(blockErr == errAuthorizationSuccess); + } else { + // A right already exists (err == noErr) or any other error occurs, we + // assume that it has been set up in advance by the system administrator or + // this is the second time we've run. Either way, there's nothing more for + // us to do. + } + }]; +} + ++ (NSString *)authorizationRightForCommand:(SEL)command + // See comment in header. +{ + return [self commandInfo][NSStringFromSelector(command)][kCommandKeyAuthRightName]; +} + + +@end diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 3012823b..c51536b3 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -54,6 +54,8 @@ CB62FC4824B132B300ADBC40 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB62FC4924B1330700ADBC40 /* LaunchctlHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */; }; CB6840A5247A3E5500E51564 /* SelfControlKillerIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */; }; + CB69C4EE25A3FD8A0030CFCD /* SCXPCAuthorization.m in Sources */ = {isa = PBXBuildFile; fileRef = CB69C4ED25A3FD8A0030CFCD /* SCXPCAuthorization.m */; }; + CB69C4EF25A3FD8A0030CFCD /* SCXPCAuthorization.m in Sources */ = {isa = PBXBuildFile; fileRef = CB69C4ED25A3FD8A0030CFCD /* SCXPCAuthorization.m */; }; CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */ = {isa = PBXBuildFile; fileRef = CB73615F19E4FDA000E0924F /* AllowlistScraper.m */; }; CB74D1152480E506002B2079 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB32D2AD21902D9D00B8CD68 /* IOKit.framework */; }; CB74D1162480E506002B2079 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E901D0F397FFA006DE6E4 /* Security.framework */; }; @@ -241,6 +243,8 @@ CB62FC3C24B1298500ADBC40 /* SCDaemonBlockMethods.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonBlockMethods.h; sourceTree = ""; }; CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonBlockMethods.m; sourceTree = ""; }; CB6840A4247A3E5500E51564 /* SelfControlKillerIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = SelfControlKillerIcon.icns; sourceTree = ""; }; + CB69C4EC25A3FD8A0030CFCD /* SCXPCAuthorization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCXPCAuthorization.h; sourceTree = ""; }; + CB69C4ED25A3FD8A0030CFCD /* SCXPCAuthorization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCXPCAuthorization.m; sourceTree = ""; }; CB6ED5662085C70200012817 /* da */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = da; path = da.lproj/FirstTime.xib; sourceTree = ""; }; CB7026181CD7177400D7C7F0 /* fr */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CB7026191CD7177400D7C7F0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -569,6 +573,8 @@ CB4294DF0F53D865008E10CA /* Classes */ = { isa = PBXGroup; children = ( + CB69C4EC25A3FD8A0030CFCD /* SCXPCAuthorization.h */, + CB69C4ED25A3FD8A0030CFCD /* SCXPCAuthorization.m */, CBD266E111ED84F700042CD8 /* SelfControlCommon.h */, CB25806416C237F10059C99A /* NSString+IPAddress.h */, CB25806516C237F10059C99A /* NSString+IPAddress.m */, @@ -1159,6 +1165,7 @@ buildActionMask = 2147483647; files = ( 8D11072D0486CEB800E47090 /* main.m in Sources */, + CB69C4EE25A3FD8A0030CFCD /* SCXPCAuthorization.m in Sources */, CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */, CBB1731920F05C07007FCAE9 /* SCUtilities.m in Sources */, F5B8CBEE19EE21C30026F3A5 /* SCTimeIntervalFormatter.m in Sources */, @@ -1206,6 +1213,7 @@ CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */, CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */, + CB69C4EF25A3FD8A0030CFCD /* SCXPCAuthorization.m in Sources */, CB62FC4324B1329500ADBC40 /* PacketFilter.m in Sources */, CB62FC4224B1329200ADBC40 /* BlockManager.m in Sources */, ); @@ -1554,6 +1562,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.8; ONLY_ACTIVE_ARCH = YES; + OTHER_CODE_SIGN_FLAGS = "-o library"; PROVISIONING_PROFILE = ""; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; @@ -1600,6 +1609,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.8; ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = "-o library"; PROVISIONING_PROFILE = ""; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; From 01c9b91b0f748fcb4e1aa1f964575de8812e6723 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 4 Jan 2021 19:04:41 -0800 Subject: [PATCH 19/72] more hardened codesigning --- SelfControl.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index c51536b3..d5ab3854 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -1562,7 +1562,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.8; ONLY_ACTIVE_ARCH = YES; - OTHER_CODE_SIGN_FLAGS = "-o library"; + OTHER_CODE_SIGN_FLAGS = "-o library,hard,kill,runtime"; PROVISIONING_PROFILE = ""; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; @@ -1609,7 +1609,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.8; ONLY_ACTIVE_ARCH = NO; - OTHER_CODE_SIGN_FLAGS = "-o library"; + OTHER_CODE_SIGN_FLAGS = "-o library,hard,kill,runtime"; PROVISIONING_PROFILE = ""; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; From 54a32761dbd705210dc1b6495dd984dffb4b4ab2 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 4 Jan 2021 19:20:08 -0800 Subject: [PATCH 20/72] finish code sign hardening --- Daemon/SCDaemon.m | 7 +++++-- Info.plist | 2 +- SelfControl.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index c52acb6d..d7aaa4d9 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -51,9 +51,12 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne (id)kSecGuestAttributeAudit: [NSData dataWithBytes: &auditToken length: sizeof(audit_token_t)] }; SecCodeRef guest; - SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(guestAttributes), kSecCSDefaultFlags, &guest); + if (SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(guestAttributes), kSecCSDefaultFlags, &guest) != errSecSuccess) { + return NO; + } + SecRequirementRef isSelfControlApp; - SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7)"), kSecCSDefaultFlags, &isSelfControlApp); + SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\" and info [CFBundleShortVersionString] >= \"4.0\" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7)"), kSecCSDefaultFlags, &isSelfControlApp); OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp); if (clientValidityStatus) { diff --git a/Info.plist b/Info.plist index 9ffa70bf..702bf8bf 100755 --- a/Info.plist +++ b/Info.plist @@ -58,7 +58,7 @@ SMPrivilegedExecutables org.eyebeam.selfcontrold - anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) + anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) SUEnableAutomaticChecks diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index d5ab3854..56846df6 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -1478,7 +1478,7 @@ INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Applications"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MARKETING_VERSION = 3.0.3; + MARKETING_VERSION = "4.0 alpha"; PRODUCT_BUNDLE_IDENTIFIER = "org.eyebeam.${PRODUCT_NAME:identifier}"; PRODUCT_NAME = SelfControl; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1511,7 +1511,7 @@ INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(HOME)/Applications"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MARKETING_VERSION = 3.0.3; + MARKETING_VERSION = "4.0 alpha"; PRODUCT_BUNDLE_IDENTIFIER = "org.eyebeam.${PRODUCT_NAME:identifier}"; PRODUCT_NAME = SelfControl; PROVISIONING_PROFILE_SPECIFIER = ""; From 0ee432c7d5d14185ec9c8a2859762580b44f4d58 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 4 Jan 2021 22:53:46 -0800 Subject: [PATCH 21/72] Auth now working, BUT asking for a password every time (no timeout) --- AppController.m | 4 ++-- BlockManager.m | 1 - Daemon/SCDaemonBlockMethods.m | 3 --- Daemon/SCDaemonXPC.m | 14 ++++++++++++++ DomainListWindowController.h | 1 - DomainListWindowController.m | 1 - HostFileBlocker.m | 5 +---- SCAppXPC.h | 4 ++-- SCAppXPC.m | 6 +++--- SCXPCAuthorization.m | 35 ++++++++++++++++++++++------------- 10 files changed, 44 insertions(+), 30 deletions(-) diff --git a/AppController.m b/AppController.m index c169c495..954a86fa 100755 --- a/AppController.m +++ b/AppController.m @@ -426,6 +426,8 @@ - (BOOL)networkConnectionIsAvailable { - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { NSLog(@"addToBlocklist: %@", host); + // Note we RETRIEVE the latest list from settings (ActiveBlocklist), but we SET the new list in defaults + // since the helper daemon should be the only one changing ActiveBlocklist NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; NSArray* cleanedEntries = [SCUtilities cleanBlocklistEntry: host]; @@ -626,7 +628,6 @@ - (void)installBlock { blocklist: [self->defaults_ arrayForKey: @"Blocklist"] isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"] endDate: [self->settings_ valueForKey: @"BlockEndDate"] - authorization: [NSData new] reply:^(NSError * _Nonnull error) { NSLog(@"WOO started block with error %@", error); if (error != nil) { @@ -657,7 +658,6 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { NSLog(@"Refreshed connection updating active blocklist!"); [self.xpc updateBlocklistWithControllingUID: getuid() newBlocklist: [self->defaults_ arrayForKey: @"Blocklist"] - authorization: [NSData new] reply:^(NSError * _Nonnull error) { NSLog(@"WOO updated block with error %@", error); diff --git a/BlockManager.m b/BlockManager.m index 512f073c..a175c2d8 100644 --- a/BlockManager.m +++ b/BlockManager.m @@ -174,7 +174,6 @@ - (void)addBlockEntryFromString:(NSString*)entry { } - (void)addBlockEntries:(NSArray*)blockList { - NSLog(@"addBlockEntries %@", blockList); for(int i = 0; i < [blockList count]; i++) { NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{ [self addBlockEntryFromString: blockList[i]]; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 4beec762..bbcf7006 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -108,7 +108,6 @@ + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*) includeCommonSubdomains: [settings boolForKey: @"AllowLocalNetworks"] includeLinkedDomains: [settings boolForKey: @"IncludeLinkedDomains"]]; [blockManager enterAppendMode]; - NSLog(@"adding block entries for %@ (diffed new arr %@ from old %@)", added, newBlocklist, activeBlocklist); [blockManager addBlockEntries: added]; [blockManager finishAppending]; @@ -128,10 +127,8 @@ + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*) } + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { - NSLog(@"ChEcKup about to hit synchronized part"); // TODO: is synchronized the wrong tool here? it could be several seconds for the block to start = a bunch of checkup threads piling up @synchronized (self) { - NSLog(@"checkup synchronized starting!"); SCSettings* settings = [SCSettings settingsForUser: controllingUID]; if(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]) { diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 4e313cd2..33dabe61 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -24,6 +24,8 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { NSLog(@"XPC method called: updateBlocklistWithControllingUID"); + NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; + if (error != nil) { + NSLog(@"ERROR: XPC authorization failed due to error %@", error); + reply(error); + return; + } else { + NSLog(@"AUTHORIZATION ACCEPTED for updateBlocklist with authData %@ and command %s", authData, sel_getName(_cmd)); + } + [SCDaemonBlockMethods updateBlocklist: controllingUID newBlocklist: newBlocklist authorization: authData reply: reply]; } - (BOOL) checkup { NSLog(@"XPC method called: checkup"); + + // no authorization needed to run a checkup + return YES; } diff --git a/DomainListWindowController.h b/DomainListWindowController.h index 04116f5b..ce26e335 100755 --- a/DomainListWindowController.h +++ b/DomainListWindowController.h @@ -36,7 +36,6 @@ IBOutlet NSTableView* domainListTableView_; IBOutlet NSMatrix* allowlistRadioMatrix_; NSUserDefaults* defaults_; - SCSettings* settings_; } // Called when the add button is clicked. Adds a new empty string to the domain diff --git a/DomainListWindowController.m b/DomainListWindowController.m index 51cb1a82..8c6d5f8b 100755 --- a/DomainListWindowController.m +++ b/DomainListWindowController.m @@ -31,7 +31,6 @@ - (DomainListWindowController*)init { if(self = [super initWithWindowNibName:@"DomainList"]) { defaults_ = [NSUserDefaults standardUserDefaults]; - settings_ = [SCSettings currentUserSettings]; NSArray* curArray = [defaults_ arrayForKey: @"Blocklist"]; if(curArray == nil) diff --git a/HostFileBlocker.m b/HostFileBlocker.m index 248d4edf..f6811e15 100755 --- a/HostFileBlocker.m +++ b/HostFileBlocker.m @@ -137,7 +137,6 @@ - (void)addRuleBlockingDomain:(NSString*)domainName { - (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName { [strLock lock]; - NSLog(@"host file blocker: append rule to block domain %@", domainName); NSRange footerLocation = [newFileContents rangeOfString: kHostFileBlockerSelfControlFooter]; if (footerLocation.location == NSNotFound) { // we can't append if a block isn't in the file already! @@ -150,9 +149,7 @@ - (void)appendExistingBlockWithRuleForDomain:(NSString*)domainName { for (NSString* ruleString in ruleStrings) { [combinedRuleString appendString: ruleString]; } - - NSLog(@"inserting combined rule string %@ at %d", combinedRuleString, footerLocation.location); - + [newFileContents insertString: combinedRuleString atIndex: footerLocation.location]; } [strLock unlock]; diff --git a/SCAppXPC.h b/SCAppXPC.h index 54eba955..2d452579 100644 --- a/SCAppXPC.h +++ b/SCAppXPC.h @@ -17,8 +17,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)connectAndExecuteCommandBlock:(void(^)(NSError *))commandBlock; - (void)getVersion; -- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; -- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate reply:(void(^)(NSError* error))reply; +- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply; @end diff --git a/SCAppXPC.m b/SCAppXPC.m index 28fc0845..42f2f2fc 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -230,7 +230,7 @@ - (void)getVersion { }]; } -- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { +- (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate reply:(void(^)(NSError* error))reply { NSLog(@"sending install command block"); [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { @@ -248,7 +248,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { +- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply { [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Blocklist update failed with connection error: %@", connectError); @@ -257,7 +257,7 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { NSLog(@"Blocklist update command failed with remote object proxy error: %@", proxyError); reply(proxyError); - }] updateBlocklistWithControllingUID: controllingUID newBlocklist: newBlocklist authorization: [NSData new] reply:^(NSError* error) { + }] updateBlocklistWithControllingUID: controllingUID newBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { NSLog(@"Blocklist update failed with error = %@\n", error); reply(error); }]; diff --git a/SCXPCAuthorization.m b/SCXPCAuthorization.m index 4591fafc..a26d54cf 100644 --- a/SCXPCAuthorization.m +++ b/SCXPCAuthorization.m @@ -15,6 +15,8 @@ @implementation SCXPCAuthorization static NSString * kCommandKeyAuthRightDefault = @"authRightDefault"; static NSString * kCommandKeyAuthRightDesc = @"authRightDescription"; +static NSDictionary* kAuthorizationRuleAuthenticateAsAdmin5MinTimeout; + // copied from Apple's Even Better Authorization Sample code + (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command // Check that the client denoted by authData is allowed to run the specified command. @@ -78,31 +80,38 @@ + (NSDictionary *)commandInfo static dispatch_once_t sOnceToken; static NSDictionary * sCommandInfo; + // static var needs to bre defined before first use + if (kAuthorizationRuleAuthenticateAsAdmin5MinTimeout == nil) { + kAuthorizationRuleAuthenticateAsAdmin5MinTimeout = @{ + @"class": @"user", + @"group": @"admin", + @"timeout": @300, // 5 minutes + @"version": @1 // not entirely sure what this does TBH + }; + } + dispatch_once(&sOnceToken, ^{ sCommandInfo = @{ NSStringFromSelector(@selector(startBlockWithControllingUID:blocklist:isAllowlist:endDate:authorization:reply:)) : @{ kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.startBlock", - kCommandKeyAuthRightDefault : @kAuthorizationRuleAuthenticateAsAdmin, + kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, kCommandKeyAuthRightDesc : NSLocalizedString( @"SelfControl needs your username and password to start the block.", - @"prompt shown when user is required to authorize to read the license key" + @"prompt shown when user is required to authorize to start block" ) }, - NSStringFromSelector(@selector(writeLicenseKey:authorization:withReply:)) : @{ - kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.writeLicenseKey", - kCommandKeyAuthRightDefault : @kAuthorizationRuleAuthenticateAsAdmin, + NSStringFromSelector(@selector(updateBlocklistWithControllingUID:newBlocklist:authorization:reply:)) : @{ + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.updateBlocklist", + kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, kCommandKeyAuthRightDesc : NSLocalizedString( - @"EBAS is trying to write its license key.", - @"prompt shown when user is required to authorize to write the license key" + @"SelfControl needs your username and password to modify the blocklist", + @"prompt shown when user is required to authorize to add to their blocklist" ) }, - NSStringFromSelector(@selector(bindToLowNumberPortAuthorization:withReply:)) : @{ - kCommandKeyAuthRightName : @"com.example.apple-samplecode.EBAS.startWebService", + NSStringFromSelector(@selector(checkupBlockWithControllingUID:)) : @{ + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.checkupBlock", kCommandKeyAuthRightDefault : @kAuthorizationRuleClassAllow, - kCommandKeyAuthRightDesc : NSLocalizedString( - @"EBAS is trying to start its web service.", - @"prompt shown when user is required to authorize to start the web service" - ) + kCommandKeyAuthRightDesc : @"" // this string should never be shown since there are no auth restrictions } }; }); From e122c5b9d9a51659b768b2ecf6599b336b9be128 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 5 Jan 2021 00:04:48 -0800 Subject: [PATCH 22/72] daemon method authorization basically works, just doesnt respect timeout caching --- Daemon/SCDaemon.m | 2 ++ SCAppXPC.m | 2 +- SCXPCAuthorization.m | 11 +++-------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index d7aaa4d9..bbe60a20 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -56,6 +56,8 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne } SecRequirementRef isSelfControlApp; + // versions before 4.0 didn't have hardened code signing, so aren't trustworthy to talk to the daemon + // (plus the daemon didn't exist before 4.0 so there's really no reason they should want to run it!) SecRequirementCreateWithString(CFSTR("anchor apple generic and identifier \"org.eyebeam.SelfControl\" and info [CFBundleShortVersionString] >= \"4.0\" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7)"), kSecCSDefaultFlags, &isSelfControlApp); OSStatus clientValidityStatus = SecCodeCheckValidity(guest, kSecCSDefaultFlags, isSelfControlApp); diff --git a/SCAppXPC.m b/SCAppXPC.m index 42f2f2fc..dc845f22 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -35,7 +35,7 @@ - (void)setupAuthorization { // with an assert. In the release build we continue with self->_authRef as NULL, which will // cause all authorized operations to fail. - err = AuthorizationCreate(NULL, NULL, 0, &self->_authRef); + err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, 0, &self->_authRef); if (err == errAuthorizationSuccess) { err = AuthorizationMakeExternalForm(self->_authRef, &extForm); self.authorization = [[NSData alloc] initWithBytes: &extForm length: sizeof(extForm)]; diff --git a/SCXPCAuthorization.m b/SCXPCAuthorization.m index a26d54cf..123786cf 100644 --- a/SCXPCAuthorization.m +++ b/SCXPCAuthorization.m @@ -56,7 +56,7 @@ + (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command err = AuthorizationCopyRights( authRef, &rights, - NULL, + kAuthorizationEmptyEnvironment, kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed, NULL ); @@ -85,7 +85,7 @@ + (NSDictionary *)commandInfo kAuthorizationRuleAuthenticateAsAdmin5MinTimeout = @{ @"class": @"user", @"group": @"admin", - @"timeout": @300, // 5 minutes + @"timeout": @(300), // 5 minutes @"version": @1 // not entirely sure what this does TBH }; } @@ -101,17 +101,12 @@ + (NSDictionary *)commandInfo ) }, NSStringFromSelector(@selector(updateBlocklistWithControllingUID:newBlocklist:authorization:reply:)) : @{ - kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.updateBlocklist", + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, kCommandKeyAuthRightDesc : NSLocalizedString( @"SelfControl needs your username and password to modify the blocklist", @"prompt shown when user is required to authorize to add to their blocklist" ) - }, - NSStringFromSelector(@selector(checkupBlockWithControllingUID:)) : @{ - kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.checkupBlock", - kCommandKeyAuthRightDefault : @kAuthorizationRuleClassAllow, - kCommandKeyAuthRightDesc : @"" // this string should never be shown since there are no auth restrictions } }; }); From a9e0c3ea662a8822602a9007202c7f1759adc716 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 9 Jan 2021 18:19:09 -0800 Subject: [PATCH 23/72] Make extending block end date go through the daemon (so it requires auth) --- AppController.m | 73 +++++++++++++++++++++-------------- Daemon/SCDaemonBlockMethods.h | 2 + Daemon/SCDaemonBlockMethods.m | 44 +++++++++++++++++++++ Daemon/SCDaemonProtocol.h | 2 + Daemon/SCDaemonXPC.m | 15 +++++++ SCAppXPC.h | 1 + SCAppXPC.m | 17 ++++++++ SCXPCAuthorization.m | 8 ++++ 8 files changed, 134 insertions(+), 28 deletions(-) diff --git a/AppController.m b/AppController.m index 954a86fa..ba534c04 100755 --- a/AppController.m +++ b/AppController.m @@ -501,13 +501,14 @@ - (void)extendBlockTime:(NSInteger)minutesToAdd lock:(NSLock*)lock { return; } - - [NSThread detachNewThreadSelector: @selector(extendBlockDuration:) - toTarget: self - withObject: @{ - @"lock": lock, - @"minutesToAdd": @(minutesToAdd) - }]; + + [self updateBlockEndDate: lock minutesToAdd: minutesToAdd]; +// [NSThread detachNewThreadSelector: @selector(extendBlockDuration:) +// toTarget: self +// withObject: @{ +// @"lock": lock, +// @"minutesToAdd": @(minutesToAdd) +// }]; } - (void)dealloc { @@ -653,7 +654,6 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid [settings_ synchronizeSettings]; - // ok, the new helper tool is installed! refresh the connection, then it's time to start the block [self.xpc refreshConnectionAndRun:^{ NSLog(@"Refreshed connection updating active blocklist!"); [self.xpc updateBlocklistWithControllingUID: getuid() @@ -685,31 +685,48 @@ - (void)setDefaultsBlockDurationOnMainThread:(NSNumber*)newBlockDuration { [defaults_ synchronize]; } -- (void)extendBlockDuration:(NSDictionary*)options { - NSInteger minutesToAdd = [options[@"minutesToAdd"] integerValue]; +- (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToAdd { + NSLog(@"updateBlockEndDate"); + if(![lockToUse tryLock]) { + return; + } + minutesToAdd = MAX(minutesToAdd, 0); // make sure there's no funny business with negative minutes - NSDate* oldBlockEndDate = [settings_ valueForKey: @"BlockEndDate"]; NSDate* newBlockEndDate = [oldBlockEndDate dateByAddingTimeInterval: (minutesToAdd * 60)]; - - // Before we try to extend the block, make sure the block time didn't run out (or is about to run out) in the meantime - if (![SCUtilities blockShouldBeRunningInDictionary: settings_.dictionaryRepresentation] || [oldBlockEndDate timeIntervalSinceNow] < 1) { - // we're done, or will be by the time we get to it! so just let it expire. they can restart it. - return; - } - // set the new block end date - [settings_ setValue: newBlockEndDate forKey: @"BlockEndDate"]; - - // synchronize it to disk to the helper tool knows immediately + // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid [settings_ synchronizeSettings]; - - // TODO: send configuration changed notification so the helper tool knows faster? - - // let the timer know it needs to recalculate - [timerWindowController_ performSelectorOnMainThread:@selector(blockEndDateUpdated) - withObject: nil - waitUntilDone: YES]; + + [self.xpc refreshConnectionAndRun:^{ + // Before we try to extend the block, make sure the block time didn't run out (or is about to run out) in the meantime + if (![SCUtilities blockShouldBeRunningInDictionary: self->settings_.dictionaryRepresentation] || [oldBlockEndDate timeIntervalSinceNow] < 1) { + // we're done, or will be by the time we get to it! so just let it expire. they can restart it. + [lockToUse unlock]; + return; + } + + NSLog(@"Refreshed connection updating active block end date!"); + [self.xpc updateBlockEndDateWithControllingUID: getuid() + newEndDate: newBlockEndDate + reply:^(NSError * _Nonnull error) { + NSLog(@"WOO updated block end date with error %@", error); + + [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; + // let the timer know it needs to recalculate + [self->timerWindowController_ performSelectorOnMainThread:@selector(blockEndDateUpdated) + withObject: nil + waitUntilDone: YES]; + + if (error != nil) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } + + [lockToUse unlock]; + }]; + }]; } - (IBAction)save:(id)sender { diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index c9663326..459b4ffd 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; ++ (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + @end NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index bbcf7006..3aa52ff8 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -126,6 +126,50 @@ + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*) } } ++ (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + @synchronized (self) { + NSLog(@"updating block end date in methods"); + if (!blockIsRunningInSettingsOrDefaults(controllingUID)) { + NSLog(@"ERROR: Can't update block end date since block isn't running"); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -213 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Updating block end date, but no block is currently running", nil) + }]; + reply(err); + return; + } + + SCSettings* settings = [SCSettings settingsForUser: controllingUID]; + + // this can only be used to *extend* the block end date - not shorten it! + // and we also won't let them extend by more than 24 hours at a time, for safety... + // TODO: they should be able to extend up to MaxBlockLength minutes, right? + NSDate* currentEndDate = [settings valueForKey: @"BlockEndDate"]; + if ([newEndDate timeIntervalSinceDate: currentEndDate] < 0) { + NSLog(@"ERROR: Can't update block end date to an earlier date"); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -301 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Can't update block end date to an earlier date", nil) + }]; + reply(err); + } + if ([newEndDate timeIntervalSinceDate: currentEndDate] > 86400) { // 86400 seconds = 1 day + NSLog(@"ERROR: Can't extend block end date by more than 1 day at a time"); + NSError* err = [NSError errorWithDomain: kSelfControlErrorDomain code: -302 userInfo: @{ + NSLocalizedDescriptionKey: NSLocalizedString(@"Can't extend block end date by more than 1 day at a time", nil) + }]; + reply(err); + } + + [settings setValue: newEndDate forKey: @"BlockEndDate"]; + [settings synchronizeSettings]; // make sure everyone knows about our new end date + + // TODO: is this still necessary in the new daemon world? + sendConfigurationChangedNotification(); + + NSLog(@"INFO: Block successfully extended."); + reply(nil); + } +} + + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { // TODO: is synchronized the wrong tool here? it could be several seconds for the block to start = a bunch of checkup threads piling up @synchronized (self) { diff --git a/Daemon/SCDaemonProtocol.h b/Daemon/SCDaemonProtocol.h index fb5a58d8..564498bf 100644 --- a/Daemon/SCDaemonProtocol.h +++ b/Daemon/SCDaemonProtocol.h @@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; + - (BOOL) checkup; - (void)getVersionWithReply:(void(^)(NSString * version))reply; diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 33dabe61..a1018ebf 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -46,6 +46,21 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS [SCDaemonBlockMethods updateBlocklist: controllingUID newBlocklist: newBlocklist authorization: authData reply: reply]; } +- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"XPC method called: updateBlockEndDateWithControllingUID"); + + NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; + if (error != nil) { + NSLog(@"ERROR: XPC authorization failed due to error %@", error); + reply(error); + return; + } else { + NSLog(@"AUTHORIZATION ACCEPTED for updateBlockENdDate with authData %@ and command %s", authData, sel_getName(_cmd)); + } + + [SCDaemonBlockMethods updateBlockEndDate:controllingUID newEndDate: newEndDate authorization: authData reply: reply]; +} + - (BOOL) checkup { NSLog(@"XPC method called: checkup"); diff --git a/SCAppXPC.h b/SCAppXPC.h index 2d452579..90bd2245 100644 --- a/SCAppXPC.h +++ b/SCAppXPC.h @@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)getVersion; - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate reply:(void(^)(NSError* error))reply; - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply; +- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))reply; @end diff --git a/SCAppXPC.m b/SCAppXPC.m index dc845f22..d6defa39 100644 --- a/SCAppXPC.m +++ b/SCAppXPC.m @@ -265,6 +265,23 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS }]; } +- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))reply { + [self connectAndExecuteCommandBlock:^(NSError * connectError) { + if (connectError != nil) { + NSLog(@"Block end date update failed with connection error: %@", connectError); + reply(connectError); + } else { + [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { + NSLog(@"Block end date update command failed with remote object proxy error: %@", proxyError); + reply(proxyError); + }] updateBlockEndDateWithControllingUID: controllingUID newEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { + NSLog(@"Block end date update failed with error = %@\n", error); + reply(error); + }]; + } + }]; +} + - (NSString*)selfControlHelperToolPath { static NSString* path; diff --git a/SCXPCAuthorization.m b/SCXPCAuthorization.m index 123786cf..d1d2ad99 100644 --- a/SCXPCAuthorization.m +++ b/SCXPCAuthorization.m @@ -107,6 +107,14 @@ + (NSDictionary *)commandInfo @"SelfControl needs your username and password to modify the blocklist", @"prompt shown when user is required to authorize to add to their blocklist" ) + }, + NSStringFromSelector(@selector(updateBlockEndDateWithControllingUID:newEndDate:authorization:reply:)) : @{ + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", + kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, + kCommandKeyAuthRightDesc : NSLocalizedString( + @"SelfControl needs your username and password to extend the block", + @"prompt shown when user is required to authorize to extend their blockc" + ) } }; }); From 234f1d950ac3a8b653d18e5688a30c77047af5ee Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 9 Jan 2021 19:15:03 -0800 Subject: [PATCH 24/72] Move all file writes to the daemon with no permissions for client to do it directly. WOO --- AppController.m | 31 ++++++++++++---- .../PreferencesAdvancedViewController.xib | 12 +++---- .../PreferencesGeneralViewController.xib | 9 ++--- Daemon/SCDaemonBlockMethods.h | 2 +- Daemon/SCDaemonBlockMethods.m | 11 +++++- Daemon/SCDaemonProtocol.h | 2 +- Daemon/SCDaemonXPC.m | 4 +-- PreferencesAdvancedViewController.h | 5 --- PreferencesAdvancedViewController.m | 36 ------------------- PreferencesGeneralViewController.h | 1 - PreferencesGeneralViewController.m | 28 --------------- SCAppXPC.h | 2 +- SCAppXPC.m | 4 +-- SCSettings.m | 21 +++++------ SCUtilities.h | 1 - SCUtilities.m | 11 ------ SCXPCAuthorization.m | 2 +- 17 files changed, 65 insertions(+), 117 deletions(-) diff --git a/AppController.m b/AppController.m index ba534c04..9f505f84 100755 --- a/AppController.m +++ b/AppController.m @@ -60,7 +60,13 @@ - (AppController*) init { @"MaxBlockLength": @1440, @"BlockLengthInterval": @15, @"WhitelistAlertSuppress": @NO, - @"GetStartedShown": @NO + @"GetStartedShown": @NO, + @"EvaluateCommonSubdomains": @YES, + @"IncludeLinkedDomains": @YES, + @"BlockSoundShouldPlay": @NO, + @"BlockSound": @5, + @"ClearCaches": @YES, + @"AllowLocalNetworks": @YES }; [defaults_ registerDefaults:appDefaults]; @@ -616,8 +622,9 @@ - (void)installBlock { } else { // helper tool installed successfully, let's prepare to start the block! // for legacy reasons, BlockDuration is in minutes, so convert it to seconds before passing it through] - NSTimeInterval blockDurationSecs = [[self->defaults_ valueForKey: @"BlockDuration"] intValue] * 60; - [SCUtilities startBlockInSettings: self->settings_ withBlockDuration: blockDurationSecs]; + // sanity check duration (must be above zero) + NSTimeInterval blockDurationSecs = MAX([[self->defaults_ valueForKey: @"BlockDuration"] intValue] * 60, 0); + NSDate* newBlockEndDate = [NSDate dateWithTimeIntervalSinceNow: blockDurationSecs]; // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid [self->settings_ synchronizeSettings]; @@ -628,7 +635,15 @@ - (void)installBlock { [self.xpc startBlockWithControllingUID: getuid() blocklist: [self->defaults_ arrayForKey: @"Blocklist"] isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"] - endDate: [self->settings_ valueForKey: @"BlockEndDate"] + endDate: newBlockEndDate + blockSettings: @{ + @"ClearCaches": [self->defaults_ valueForKey: @"ClearCaches"], + @"AllowLocalNetworks": [self->defaults_ valueForKey: @"AllowLocalNetworks"], + @"EvaluateCommonSubdomains": [self->defaults_ valueForKey: @"EvaluateCommonSubdomains"], + @"IncludeLinkedDomains": [self->defaults_ valueForKey: @"IncludeLinkedDomains"], + @"BlockSoundShouldPlay": [self->defaults_ valueForKey: @"BlockSoundShouldPlay"], + @"BlockSound": [self->defaults_ valueForKey: @"BlockSound"] + } reply:^(NSError * _Nonnull error) { NSLog(@"WOO started block with error %@", error); if (error != nil) { @@ -636,8 +651,12 @@ - (void)installBlock { withObject: error waitUntilDone: YES]; } - self.addingBlock = false; - [self refreshUserInterface]; + + // get the new settings + [settings_ synchronizeSettingsWithCompletion:^(NSError * _Nullable error) { + self.addingBlock = false; + [self refreshUserInterface]; + }]; }]; }]; } diff --git a/Base.lproj/PreferencesAdvancedViewController.xib b/Base.lproj/PreferencesAdvancedViewController.xib index c0f4e3fb..4602c243 100644 --- a/Base.lproj/PreferencesAdvancedViewController.xib +++ b/Base.lproj/PreferencesAdvancedViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -29,7 +29,7 @@ - + diff --git a/Base.lproj/PreferencesGeneralViewController.xib b/Base.lproj/PreferencesGeneralViewController.xib index 189222f2..4ea1eab1 100644 --- a/Base.lproj/PreferencesGeneralViewController.xib +++ b/Base.lproj/PreferencesGeneralViewController.xib @@ -1,14 +1,13 @@ - + - + - @@ -39,7 +38,7 @@ - + - + @@ -73,12 +73,23 @@ - + + - + diff --git a/Common/SCSentry.h b/Common/SCSentry.h index 81724176..63806ed0 100644 --- a/Common/SCSentry.h +++ b/Common/SCSentry.h @@ -13,8 +13,11 @@ NS_ASSUME_NONNULL_BEGIN @interface SCSentry : NSObject + (void)startSentry:(NSString*)componentId; ++ (void)addBreadcrumb:(NSString*)message category:(NSString*)category; + (void)captureError:(NSError*)error; ++ (void)captureMessage:(NSString*)message withScopeBlock:(nullable void (^)(SentryScope * _Nonnull))block; + (void)captureMessage:(NSString*)message; ++ (void)showErrorReportingPromptIfNeeded; @end diff --git a/Common/SCSentry.m b/Common/SCSentry.m index 3000a152..a00b9192 100644 --- a/Common/SCSentry.m +++ b/Common/SCSentry.m @@ -18,12 +18,58 @@ + (void)startSentry:(NSString*)componentId { options.releaseName = [NSString stringWithFormat: @"%@%@", componentId, SELFCONTROL_VERSION_STRING]; options.enableAutoSessionTracking = NO; options.environment = @"dev"; + + // make sure no data leaves the device if error reporting isn't enabled + options.beforeSend = ^SentryEvent * _Nullable(SentryEvent * _Nonnull event) { + if ([SCSentry errorReportingEnabled]) { + return event; + } else { + return NULL; + } + }; }]; [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { [scope setTagValue: [[NSLocale currentLocale] localeIdentifier] forKey: @"localeId"]; }]; } ++ (BOOL)errorReportingEnabled { + if (geteuid() != 0) { + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + return [defaults boolForKey: @"EnableErrorReporting"]; + } else { + // since we're root, we've gotta see what's in SCSettings (where the user's defaults will have been copied) + return [[SCSettings sharedSettings] boolForKey: @"EnableErrorReporting"]; + } +} + ++ (void)showErrorReportingPromptIfNeeded { + // no need to show the prompt if we're root (aka in the CLI/daemon), or already enabled error reporting, or if the user already dismissed it + if (!geteuid()) return; + if ([SCSentry errorReportingEnabled]) return; + + // if they've already dismissed this once, don't show it again + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults boolForKey: @"ErrorReportingPromptDismissed"]) { + return; + } + + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText: NSLocalizedString(@"Enable automatic error reporting", "Title of error reporting prompt")]; + [alert setInformativeText:NSLocalizedString(@"SelfControl can automatically send bug reports to help us improve the software. All data is anonymized, your blocklist is never shared, and no identifying information is sent.", @"Message explaining error reporting")]; + [alert addButtonWithTitle: NSLocalizedString(@"Enable Error Reporting", @"Button to enable error reporting")]; + [alert addButtonWithTitle: NSLocalizedString(@"Don't Send Reports", "Button to decline error reporting")]; + + NSModalResponse modalResponse = [alert runModal]; + if (modalResponse == NSAlertFirstButtonReturn) { + [defaults setBool: YES forKey: @"EnableErrorReporting"]; + [defaults setBool: YES forKey: @"ErrorReportingPromptDismissed"]; + } else if (modalResponse == NSAlertSecondButtonReturn) { + [defaults setBool: NO forKey: @"EnableErrorReporting"]; + [defaults setBool: YES forKey: @"ErrorReportingPromptDismissed"]; + } // if the modal exited some other way, do nothing +} + + (void)updateDefaultsContext { // if we're root, we can't get defaults properly, so forget it if (!geteuid()) { @@ -32,27 +78,57 @@ + (void)updateDefaultsContext { NSMutableDictionary* defaultsDict = [[[NSUserDefaults standardUserDefaults] persistentDomainForName: @"org.eyebeam.SelfControl"] mutableCopy]; - // delete blocklist (because PII) and check time (because unnecessary, and Sentry doesn't like dates) + // delete blocklist (because PII) and update check time + // (because unnecessary, and Sentry doesn't like dates) + // but store the blocklist length as a useful piece of debug info + id blocklist = defaultsDict[@"Blocklist"]; + NSUInteger blocklistLength = (blocklist == nil) ? 0 : ((NSArray*)blocklist).count; + [defaultsDict setObject: @(blocklistLength) forKey: @"BlocklistLength"]; [defaultsDict removeObjectForKey: @"Blocklist"]; [defaultsDict removeObjectForKey: @"SULastCheckTime"]; - + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { [scope setContextValue: defaultsDict forKey: @"NSUserDefaults"]; }]; } ++ (void)addBreadcrumb:(NSString*)message category:(NSString*)category { + SentryBreadcrumb* crumb = [[SentryBreadcrumb alloc] init]; + crumb.level = kSentryLevelInfo; + crumb.category = category; + crumb.message = message; + [SentrySDK addBreadcrumb: crumb]; +} + + (void)captureError:(NSError*)error { + if (![SCSentry errorReportingEnabled]) { + return; + } + NSLog(@"Reporting error %@ to Sentry...", error); [[SCSettings sharedSettings] updateSentryContext]; [SCSentry updateDefaultsContext]; [SentrySDK captureError: error]; } -+ (void)captureMessage:(NSString*)message { ++ (void)captureMessage:(NSString*)message withScopeBlock:(nullable void (^)(SentryScope * _Nonnull))block { + if (![SCSentry errorReportingEnabled]) { + return; + } + NSLog(@"Reporting message %@ to Sentry...", message); [[SCSettings sharedSettings] updateSentryContext]; [SCSentry updateDefaultsContext]; - [SentrySDK captureMessage: message]; + + if (block != nil) { + [SentrySDK captureMessage: message withScopeBlock: block]; + } else { + [SentrySDK captureMessage: message]; + } +} + ++ (void)captureMessage:(NSString*)message { + [SCSentry captureMessage: message withScopeBlock: nil]; } @end diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index 334bdb25..77e557f1 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -218,10 +218,12 @@ - (void)getVersion:(void(^)(NSString* version, NSError* error))reply { [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Failed to get daemon version with connection error: %@", connectError); + [SCSentry captureError: connectError]; reply(nil, connectError); } else { [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { NSLog(@"Failed to get daemon version with remote object proxy error: %@", proxyError); + [SCSentry captureError: proxyError]; reply(nil, proxyError); }] getVersionWithReply:^(NSString * _Nonnull version) { reply(version, nil); @@ -233,14 +235,19 @@ - (void)getVersion:(void(^)(NSString* version, NSError* error))reply { - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate blockSettings:(NSDictionary*)blockSettings reply:(void(^)(NSError* error))reply { [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { + [SCSentry captureError: connectError]; NSLog(@"Install command failed with connection error: %@", connectError); reply(connectError); } else { [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { NSLog(@"Install command failed with remote object proxy error: %@", proxyError); + [SCSentry captureError: proxyError]; reply(proxyError); }] startBlockWithControllingUID: controllingUID blocklist: blocklist isAllowlist:isAllowlist endDate:endDate blockSettings: blockSettings authorization: self.authorization reply:^(NSError* error) { - NSLog(@"Install failed with error = %@\n", error); + if (error != nil) { + NSLog(@"Install failed with error = %@\n", error); + [SCSentry captureError: error]; + } reply(error); }]; } @@ -251,13 +258,16 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Blocklist update failed with connection error: %@", connectError); + [SCSentry captureError: connectError]; reply(connectError); } else { [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { NSLog(@"Blocklist update command failed with remote object proxy error: %@", proxyError); + [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlocklistWithControllingUID: controllingUID newBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { NSLog(@"Blocklist update failed with error = %@\n", error); + [SCSentry captureError: error]; reply(error); }]; } @@ -268,13 +278,16 @@ - (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(N [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Block end date update failed with connection error: %@", connectError); + [SCSentry captureError: connectError]; reply(connectError); } else { [[self.daemonConnection remoteObjectProxyWithErrorHandler:^(NSError * proxyError) { NSLog(@"Block end date update command failed with remote object proxy error: %@", proxyError); + [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlockEndDateWithControllingUID: controllingUID newEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { NSLog(@"Block end date update failed with error = %@\n", error); + [SCSentry captureError: error]; reply(error); }]; } diff --git a/Credits.rtf b/Credits.rtf index 44539ee3..f786a345 100755 --- a/Credits.rtf +++ b/Credits.rtf @@ -1,14 +1,15 @@ -{\rtf1\ansi\ansicpg1252\cocoartf1348\cocoasubrtf170 -{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\rtf1\ansi\ansicpg1252\cocoartf2578 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} \vieww9000\viewh8400\viewkind0 -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc\partightenfactor0 {\field{\*\fldinst{HYPERLINK "http://selfcontrolapp.com"}}{\fldrslt \f0\fs22 \cf0 http://selfcontrolapp.com}} \f0\fs22 \ \ Developed by {\field{\*\fldinst{HYPERLINK "http://charliestigler.com"}}{\fldrslt Charlie Stigler}}, {\field{\*\fldinst{HYPERLINK "http://visitsteve.com/"}}{\fldrslt Steve Lambert}}, and you?\ \ -Icon by {\field{\*\fldinst{HYPERLINK "https://github.com/josephfusco"}}{\fldrslt Joseph Fusco}}, contributions by all of {\field{\*\fldinst{HYPERLINK "https://github.com/SelfControlApp/selfcontrol/graphs/contributors"}}{\fldrslt these lovely folks}}, and translations by {\field{\*\fldinst{HYPERLINK "https://github.com/SelfControlApp/selfcontrol/wiki/Translation-Credits"}}{\fldrslt these troopers}}.\ +Icon by {\field{\*\fldinst{HYPERLINK "https://github.com/josephfusco"}}{\fldrslt Joseph Fusco}}, contributions by all of {\field{\*\fldinst{HYPERLINK "https://github.com/SelfControlApp/selfcontrol/graphs/contributors"}}{\fldrslt these lovely folks}}, and translations by {\field{\*\fldinst{HYPERLINK "https://github.com/SelfControlApp/selfcontrol/wiki/Translation-Credits"}}{\fldrslt these troopers}}. Error reporting generously provided by {\field{\*\fldinst{HYPERLINK "https://sentry.io/"}}{\fldrslt Sentry}}.\ \ Free Software created at {\field{\*\fldinst{HYPERLINK "http://eyebeam.org/"}}{\fldrslt Eyebeam}} under the {\field{\*\fldinst{HYPERLINK "http://www.gnu.org/copyleft/gpl.html"}}{\fldrslt GNU GPL}}. {\field{\*\fldinst{HYPERLINK "https://github.com/slambert/selfcontrol/"}}{\fldrslt Source code}} is available on GitHub.} \ No newline at end of file diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index c343ac8b..092f4420 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -64,6 +64,7 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne if (clientValidityStatus) { NSError* error = [NSError errorWithDomain: NSOSStatusErrorDomain code: clientValidityStatus userInfo: nil]; NSLog(@"Rejecting XPC connection because of invalid client signing. Error was %@", error); + [SCSentry captureError: error]; return NO; } @@ -74,6 +75,7 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne [newConnection resume]; NSLog(@"Accepted new connection!"); + [SCSentry addBreadcrumb: @"Daemon accepted new connection" category: @"daemon"]; return YES; } diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 4c7dacd7..07019e40 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -51,6 +51,8 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { - NSLog(@"updating blocklist in methods"); if (![SCDaemonBlockMethods lockOrTimeout: reply]) { return; } + [SCSentry addBreadcrumb: @"Daemon method updateBlocklist called" category: @"daemon"]; + [SentrySDK crash]; if ([SCUtilities legacyBlockIsRunning: controllingUID]) { NSLog(@"ERROR: Can't update blocklist because a legacy block is running"); NSError* err = [SCErr errorWithCode: 303]; @@ -178,18 +184,20 @@ + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*) // that blocked pages are not loaded from a cache. clearCachesIfRequested(controllingUID); + [SCSentry addBreadcrumb: @"Daemon updated blocklist successfully" category: @"daemon"]; NSLog(@"INFO: Blocklist successfully updated."); reply(nil); + [self.daemonMethodLock unlock]; } + (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { if (![SCDaemonBlockMethods lockOrTimeout: reply]) { - NSLog(@"NOPE IT DIDNT HAPPEN"); return; } + + [SCSentry addBreadcrumb: @"Daemon method updateBlockEndDate called" category: @"daemon"]; - NSLog(@"updating block end date in methods"); if ([SCUtilities legacyBlockIsRunning: controllingUID]) { NSLog(@"ERROR: Can't update block end date because a legacy block is running"); NSError* err = [SCErr errorWithCode: 306]; @@ -234,6 +242,7 @@ + (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate // TODO: is this still necessary in the new daemon world? sendConfigurationChangedNotification(); + [SCSentry addBreadcrumb: @"Daemon extended block successfully" category: @"daemon"]; NSLog(@"INFO: Block successfully extended."); reply(nil); [self.daemonMethodLock unlock]; @@ -243,6 +252,8 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { if (![SCDaemonBlockMethods lockOrTimeout: nil timeout: CHECKUP_LOCK_TIMEOUT]) { return; } + + [SCSentry addBreadcrumb: @"Daemon method checkupBlock called" category: @"daemon"]; SCSettings* settings = [SCSettings sharedSettings]; NSTimeInterval integrityCheckIntervalSecs = 10.0; @@ -282,6 +293,7 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { NSLog(@"INFO: Checkup ran, block expired, removing block."); removeBlock(controllingUID); + [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; [SCDaemonUtilities unloadDaemonJob]; // Execution should never reach this point. Launchd unloading the job in @@ -323,6 +335,7 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { clearCachesIfRequested(controllingUID); + [SCSentry addBreadcrumb: @"Daemon found compromised block integrity and re-added rules" category: @"daemon"]; NSLog(@"INFO: Checkup ran, readded block rules."); } else NSLog(@"INFO: Checkup ran with integrity check, no action needed."); } diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m index 0b08e7d2..ea6cdceb 100644 --- a/Daemon/SCDaemonUtilities.m +++ b/Daemon/SCDaemonUtilities.m @@ -15,6 +15,7 @@ @implementation SCDaemonUtilities + (void)unloadDaemonJob { NSLog(@"Unloading SelfControl daemon..."); + [SCSentry addBreadcrumb: @"Daemon about to unload" category: @"daemon"]; SCSettings* settings = [SCSettings sharedSettings]; // we're about to unload the launchd job diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 0b4fb478..2981b5d1 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -17,6 +17,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray Date: Fri, 15 Jan 2021 20:02:23 -0800 Subject: [PATCH 43/72] fix up issues with Sentry integration --- Common/SCSentry.h | 2 +- Common/SCSentry.m | 34 ++++++++++++++++++++++++++++------ Common/SCXPCClient.m | 12 ++++++++---- Daemon/SCDaemonBlockMethods.m | 1 - DomainListWindowController.m | 2 +- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Common/SCSentry.h b/Common/SCSentry.h index 63806ed0..63c1764e 100644 --- a/Common/SCSentry.h +++ b/Common/SCSentry.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)captureError:(NSError*)error; + (void)captureMessage:(NSString*)message withScopeBlock:(nullable void (^)(SentryScope * _Nonnull))block; + (void)captureMessage:(NSString*)message; -+ (void)showErrorReportingPromptIfNeeded; ++ (BOOL)showErrorReportingPromptIfNeeded; @end diff --git a/Common/SCSentry.m b/Common/SCSentry.m index a00b9192..0a533203 100644 --- a/Common/SCSentry.m +++ b/Common/SCSentry.m @@ -43,15 +43,16 @@ + (BOOL)errorReportingEnabled { } } -+ (void)showErrorReportingPromptIfNeeded { +// returns YES if we turned on error reporting based on the prompt return ++ (BOOL)showErrorReportingPromptIfNeeded { // no need to show the prompt if we're root (aka in the CLI/daemon), or already enabled error reporting, or if the user already dismissed it - if (!geteuid()) return; - if ([SCSentry errorReportingEnabled]) return; + if (!geteuid()) return NO; + if ([SCSentry errorReportingEnabled]) return NO; // if they've already dismissed this once, don't show it again NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; if ([defaults boolForKey: @"ErrorReportingPromptDismissed"]) { - return; + return NO; } NSAlert* alert = [[NSAlert alloc] init]; @@ -64,10 +65,13 @@ + (void)showErrorReportingPromptIfNeeded { if (modalResponse == NSAlertFirstButtonReturn) { [defaults setBool: YES forKey: @"EnableErrorReporting"]; [defaults setBool: YES forKey: @"ErrorReportingPromptDismissed"]; + return YES; } else if (modalResponse == NSAlertSecondButtonReturn) { [defaults setBool: NO forKey: @"EnableErrorReporting"]; [defaults setBool: YES forKey: @"ErrorReportingPromptDismissed"]; } // if the modal exited some other way, do nothing + + return NO; } + (void)updateDefaultsContext { @@ -102,7 +106,16 @@ + (void)addBreadcrumb:(NSString*)message category:(NSString*)category { + (void)captureError:(NSError*)error { if (![SCSentry errorReportingEnabled]) { - return; + // if we're root (CLI/daemon), we can't show prompts + if (!geteuid()) { + return; + } + + // prompt 'em to turn on error reports now if we haven't already! if they do we can continue + BOOL enabledReports = [SCSentry showErrorReportingPromptIfNeeded]; + if (!enabledReports) { + return; + } } NSLog(@"Reporting error %@ to Sentry...", error); @@ -113,7 +126,16 @@ + (void)captureError:(NSError*)error { + (void)captureMessage:(NSString*)message withScopeBlock:(nullable void (^)(SentryScope * _Nonnull))block { if (![SCSentry errorReportingEnabled]) { - return; + // if we're root (CLI/daemon), we can't show prompts + if (!geteuid()) { + return; + } + + // prompt 'em to turn on error reports now if we haven't already! if they do we can continue + BOOL enabledReports = [SCSentry showErrorReportingPromptIfNeeded]; + if (!enabledReports) { + return; + } } NSLog(@"Reporting message %@ to Sentry...", message); diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index 77e557f1..975a3e56 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -266,8 +266,10 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlocklistWithControllingUID: controllingUID newBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { - NSLog(@"Blocklist update failed with error = %@\n", error); - [SCSentry captureError: error]; + if (error != nil) { + NSLog(@"Blocklist update failed with error = %@\n", error); + [SCSentry captureError: error]; + } reply(error); }]; } @@ -286,8 +288,10 @@ - (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(N [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlockEndDateWithControllingUID: controllingUID newEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { - NSLog(@"Block end date update failed with error = %@\n", error); - [SCSentry captureError: error]; + if (error != nil) { + NSLog(@"Block end date update failed with error = %@\n", error); + [SCSentry captureError: error]; + } reply(error); }]; } diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 07019e40..8bff09a8 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -126,7 +126,6 @@ + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*) } [SCSentry addBreadcrumb: @"Daemon method updateBlocklist called" category: @"daemon"]; - [SentrySDK crash]; if ([SCUtilities legacyBlockIsRunning: controllingUID]) { NSLog(@"ERROR: Can't update blocklist because a legacy block is running"); NSError* err = [SCErr errorWithCode: 303]; diff --git a/DomainListWindowController.m b/DomainListWindowController.m index 394dc95a..fa9aa3c3 100755 --- a/DomainListWindowController.m +++ b/DomainListWindowController.m @@ -55,7 +55,7 @@ - (void)showWindow:(id)sender { if ([domainList_ count] == 0) { [self addDomain: self]; } - [SCSentry captureMessage: @"domain window test error"]; + [self updateWindowTitle]; } From 95e3fd171ccc2f6ac2556c5943e588817ae20fb4 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 16 Jan 2021 12:06:42 -0800 Subject: [PATCH 44/72] Clean up daemon unloading/exiting + syncing settings before exit --- Common/HelperCommon.h | 7 ------- Common/HelperCommon.m | 26 -------------------------- SCSettings.h => Common/SCSettings.h | 1 + SCSettings.m => Common/SCSettings.m | 26 +++++++++++++++++++++++++- Daemon/SCDaemonBlockMethods.m | 19 +++++++++---------- Daemon/SCDaemonUtilities.m | 27 +++++++++------------------ SCKillerHelper/main.m | 2 +- SelfControl.xcodeproj/project.pbxproj | 8 ++++---- 8 files changed, 49 insertions(+), 67 deletions(-) rename SCSettings.h => Common/SCSettings.h (93%) rename SCSettings.m => Common/SCSettings.m (95%) diff --git a/Common/HelperCommon.h b/Common/HelperCommon.h index 26f8b771..e0c9a806 100644 --- a/Common/HelperCommon.h +++ b/Common/HelperCommon.h @@ -60,10 +60,3 @@ void clearOSDNSCache(void); void removeBlock(uid_t controllingUID); void sendConfigurationChangedNotification(void); - -// makes sure the secured settings finish syncing -// before calling exit with the given status -// if there's any chance settings could have been modified, we should -// always call this instead of plain exit() to make sure settings aren't left unsynced! -// this waits for the settings to sync before returning, so may be slow -void syncSettingsAndExit(SCSettings* settings, int status); diff --git a/Common/HelperCommon.m b/Common/HelperCommon.m index 01108aef..62daab13 100644 --- a/Common/HelperCommon.m +++ b/Common/HelperCommon.m @@ -185,29 +185,3 @@ void sendConfigurationChangedNotification() { userInfo: nil options: NSNotificationDeliverImmediately | NSNotificationPostToAllSessions]; } - -void syncSettingsAndExit(SCSettings* settings, int status) { - // this should always be run on the main thread so it blocks main() - if (![NSThread isMainThread]) { - dispatch_sync(dispatch_get_main_queue(), ^{ - syncSettingsAndExit(settings, status); - }); - } - - [settings synchronizeSettingsWithCompletion:^(NSError* err) { - if (err != nil) { - NSLog(@"WARNING: Settings failed to synchronize before exit, with error %@", err); - } - - exit(status); - }]; - - // wait 5 seconds. assuming the synchronization completes during that time, - // it'll exit() for us and we'll never get to the other side of this wait - sleep(5); - - // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. - NSLog(@"WARNING: Settings sync timed out before exiting"); - - exit(status); -} diff --git a/SCSettings.h b/Common/SCSettings.h similarity index 93% rename from SCSettings.h rename to Common/SCSettings.h index 45addc4d..f60f9014 100644 --- a/SCSettings.h +++ b/Common/SCSettings.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)writeSettings; - (void)synchronizeSettingsWithCompletion:(nullable void(^)(NSError* _Nullable))completionBlock; - (void)synchronizeSettings; +- (void)syncSettingsAndWait:(int)timeoutSecs error:(NSError* __strong *)errPtr; - (void)setValue:(id)value forKey:(NSString*)key stopPropagation:(BOOL)stopPropagation; - (void)setValue:(nullable id)value forKey:(NSString*)key; diff --git a/SCSettings.m b/Common/SCSettings.m similarity index 95% rename from SCSettings.m rename to Common/SCSettings.m index 52bb8907..2c8a564d 100644 --- a/SCSettings.m +++ b/Common/SCSettings.m @@ -341,13 +341,37 @@ - (void)synchronizeSettingsWithCompletion:(nullable void (^)(NSError * _Nullable NSLog(@" --> Writing settings to disk (haven't been written since %@)", self.lastSynchronizedWithDisk); [self writeSettingsWithCompletion: completionBlock]; } else { - if(completionBlock != nil) completionBlock(nil); + if(completionBlock != nil) { + // don't just run the callback asynchronously, since it makes this method harder to reason about + // (it'd sometimes call back synchronously and sometimes async) +// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionBlock(nil); +// }); + } } } - (void)synchronizeSettings { [self synchronizeSettingsWithCompletion: nil]; } +- (void)syncSettingsAndWait:(int)timeoutSecs error:(NSError* __strong *)errPtr { + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + + // do this on another thread so it doesn't deadlock our semaphore + // (also dispatch_async ensures correct behavior even if synchronizeSettingsWithCompletion itself returns synchronously) + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self synchronizeSettingsWithCompletion:^(NSError* err) { + *errPtr = err; + + dispatch_semaphore_signal(sema); + }]; + }); + + if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, timeoutSecs * NSEC_PER_SEC))) { + *errPtr = [SCErr errorWithCode: 601]; + } +} + - (void)setValue:(id)value forKey:(NSString*)key stopPropagation:(BOOL)stopPropagation { // if we're a readonly instance, we generally shouldn't be allowing values to be set // the only exception is receiving value updates (via notification) from other processes diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 8bff09a8..cf998b2f 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -273,12 +273,6 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { removeBlock(controllingUID); - [SCDaemonUtilities unloadDaemonJob]; - - // execution should never reach this point because we've unloaded - syncSettingsAndExit(settings, EX_SOFTWARE); - - // get rid of this block // Temporarily disabled the TamperingDetection flag because it was sometimes causing false positives // (i.e. people having the background set repeatedly despite no attempts to cheat) // We will try to bring this feature back once we can debug it @@ -286,6 +280,11 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { // [settings setValue: @YES forKey: @"TamperingDetected"]; // [settings synchronizeSettings]; // + + [SCDaemonUtilities unloadDaemonJob]; + + // execution should never reach this point because we've unloaded + exit(EX_SOFTWARE); } if (![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { @@ -293,11 +292,11 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { removeBlock(controllingUID); [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; + [SCDaemonUtilities unloadDaemonJob]; - - // Execution should never reach this point. Launchd unloading the job in - // should have killed this process. TODO: but maybe doesn't always with a daemon? - syncSettingsAndExit(settings, EX_SOFTWARE); + + // execution should never reach this point because we've unloaded + exit(EX_SOFTWARE); } else if ([[NSDate date] timeIntervalSinceDate: lastBlockIntegrityCheck] > integrityCheckIntervalSecs) { lastBlockIntegrityCheck = [NSDate date]; // The block is still on. Every once in a while, we should diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m index ea6cdceb..a0f8c7e4 100644 --- a/Daemon/SCDaemonUtilities.m +++ b/Daemon/SCDaemonUtilities.m @@ -21,26 +21,17 @@ + (void)unloadDaemonJob { // we're about to unload the launchd job // this will kill this process, so we have to make sure // all settings are synced before we unload - [settings synchronizeSettingsWithCompletion:^(NSError* err) { - if (err != nil) { - NSLog(@"WARNING: Settings failed to synchronize before unloading daemon, with error %@", err); - } - - CFErrorRef cfError; - SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); - if (cfError) { - NSLog(@"WARNING: Failed to remove selfcontrold daemon with error %@", cfError); - } - }]; - - // wait 5 seconds. assuming the synchronization completes during that time, - // it'll unload the launchd job for us and we'll never get to the other side of this wait - sleep(5); - + NSError* syncErr; + [settings syncSettingsAndWait: 5.0 error: &syncErr]; + if (syncErr != nil) { + NSLog(@"WARNING: Sync failed or timed out with error %@ before unloading daemon job", syncErr); + [SCSentry captureError: syncErr]; + } + // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. - NSLog(@"WARNING: Settings sync timed out before unloading block"); CFErrorRef cfError; - SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); + // this should block until the process is dead, so we should never get to the other side if it's successful + SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, YES, &cfError); if (cfError) { NSLog(@"Failed to remove selfcontrold daemon with error %@", cfError); } diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index 591b16bd..0c969e4f 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -135,7 +135,7 @@ int main(int argc, char* argv[]) { [log appendFormat: @"Unloading the legacy (1.0 - 3.0.3) launchd daemon returned: %d\n\n", status]; CFErrorRef cfError; - SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, NO, &cfError); + SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, YES, &cfError); if (cfError) { [log appendFormat: @"Failed to remove selfcontrold daemon (4.x) with error %@\n\n", cfError]; } else { diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index a0557e0b..41dc32e3 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -508,7 +508,7 @@ name = Products; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* SelfControl */ = { isa = PBXGroup; children = ( CBDFFF4B24A07DB300622CEE /* SelfControl.entitlements */, @@ -593,6 +593,8 @@ CBADC27D25B22BC7000EE5BB /* SCSentry.m */, CB1465B625B027E700130D2E /* SCErr.h */, CB1465B725B027E700130D2E /* SCErr.m */, + CBF3B572217BADD7006D5F52 /* SCSettings.h */, + CBF3B573217BADD7006D5F52 /* SCSettings.m */, CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */, CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */, CB69C4EC25A3FD8A0030CFCD /* SCXPCAuthorization.h */, @@ -635,8 +637,6 @@ CBEE50C00F48C21F00F5DF1C /* TimerWindowController.m */, CBC2F8650F4674E300CF2A42 /* LaunchctlHelper.h */, CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */, - CBF3B572217BADD7006D5F52 /* SCSettings.h */, - CBF3B573217BADD7006D5F52 /* SCSettings.m */, CBB1731220F041F4007FCAE9 /* SCUtilities.h */, CBB1731320F041F4007FCAE9 /* SCUtilities.m */, CB5DFCB52251DD1F0084CEC2 /* SCConstants.h */, @@ -947,7 +947,7 @@ Base, da, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* SelfControl */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; From 7ef542bcd6a9a1b95b84ede5e1016dbb2da836c4 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 16 Jan 2021 18:39:22 -0800 Subject: [PATCH 45/72] Make cache clearing system-wide instead of per-user, so controllingUID is no longer needed for it --- Common/HelperCommon.h | 6 ++-- Common/HelperCommon.m | 36 +++----------------- SCUtilities.h => Common/SCUtilities.h | 2 ++ SCUtilities.m => Common/SCUtilities.m | 48 +++++++++++++++++++++++++++ Daemon/SCDaemon.m | 2 +- Daemon/SCDaemonBlockMethods.h | 2 +- Daemon/SCDaemonBlockMethods.m | 17 ++++++---- SCError.strings | 3 ++ SelfControl.xcodeproj/project.pbxproj | 4 +-- 9 files changed, 75 insertions(+), 45 deletions(-) rename SCUtilities.h => Common/SCUtilities.h (98%) rename SCUtilities.m => Common/SCUtilities.m (91%) diff --git a/Common/HelperCommon.h b/Common/HelperCommon.h index e0c9a806..f1aa4664 100644 --- a/Common/HelperCommon.h +++ b/Common/HelperCommon.h @@ -47,16 +47,16 @@ NSSet* getEvaluatedHostNamesFromCommonSubdomains(NSString* hostName, int port); // Checks the settings system to see whether the user wants their web browser // caches cleared, and deletes the specific cache folders for a few common // web browsers if it is required. -void clearCachesIfRequested(uid_t controllingUID); +void clearCachesIfRequested(void); // Clear only the caches for browsers -void clearBrowserCaches(uid_t controllingUID); +void clearBrowserCaches(void); // Clear only the OS-level DNS cache void clearOSDNSCache(void); // Removes block via settings, host file rules and ipfw rules, // deleting user caches if requested, and migrating legacy settings. -void removeBlock(uid_t controllingUID); +void removeBlock(void); void sendConfigurationChangedNotification(void); diff --git a/Common/HelperCommon.m b/Common/HelperCommon.m index 62daab13..b17ea442 100644 --- a/Common/HelperCommon.m +++ b/Common/HelperCommon.m @@ -100,43 +100,17 @@ void removeRulesFromFirewall() { return evaluatedAddresses; } -void clearCachesIfRequested(uid_t controllingUID) { +void clearCachesIfRequested() { SCSettings* settings = [SCSettings sharedSettings]; if(![settings boolForKey: @"ClearCaches"]) { return; } - clearBrowserCaches(controllingUID); + // TODO: do something with the NSError returned by this method + [SCUtilities clearBrowserCaches]; clearOSDNSCache(); } -void clearBrowserCaches(uid_t controllingUID) { - NSFileManager* fileManager = [NSFileManager defaultManager]; - - // need to seteuid so the tilde expansion will work properly - seteuid(controllingUID); - NSString* libraryDirectoryExpanded = [@"~/Library" stringByExpandingTildeInPath]; - seteuid(0); - - NSArray* cacheDirs = @[ - // chrome - @"/Caches/Google/Chrome/Default", - @"/Caches/Google/Chrome/com.google.Chrome", - - // firefox - @"/Caches/Firefox/Profiles", - - // safari - @"/Caches/com.apple.Safari", - @"/Containers/com.apple.Safari/Data/Library/Caches" // this one seems to fail due to permissions issues, but not sure how to fix - ]; - for (NSString* cacheDir in cacheDirs) { - NSString* absoluteCacheDir = [libraryDirectoryExpanded stringByAppendingString: cacheDir]; - NSLog(@"Clearing browser cache folder %@", absoluteCacheDir); - [fileManager removeItemAtPath: absoluteCacheDir error: nil]; - } -} - void clearOSDNSCache() { // no error checks - if it works it works! NSTask* flushDsCacheUtil = [[NSTask alloc] init]; @@ -160,7 +134,7 @@ void clearOSDNSCache() { NSLog(@"Cleared OS DNS caches"); } -void removeBlock(uid_t controllingUID) { +void removeBlock() { SCSettings* settings = [SCSettings sharedSettings]; [SCUtilities removeBlockFromSettings]; @@ -174,7 +148,7 @@ void removeBlock(uid_t controllingUID) { NSLog(@"INFO: Block cleared."); - clearCachesIfRequested(controllingUID); + clearCachesIfRequested(); } void sendConfigurationChangedNotification() { diff --git a/SCUtilities.h b/Common/SCUtilities.h similarity index 98% rename from SCUtilities.h rename to Common/SCUtilities.h index d9c375f7..cb9f115a 100644 --- a/SCUtilities.h +++ b/Common/SCUtilities.h @@ -50,6 +50,8 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu + (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; + (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL; ++ (NSError*)clearBrowserCaches; + // migration methods + (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId; + (BOOL)legacySettingsFound:(uid_t)controllingUID; diff --git a/SCUtilities.m b/Common/SCUtilities.m similarity index 91% rename from SCUtilities.m rename to Common/SCUtilities.m index ad74e53f..adc94cc1 100644 --- a/SCUtilities.m +++ b/Common/SCUtilities.m @@ -277,6 +277,54 @@ + (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL { }; } ++ (NSError*)clearBrowserCaches { + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* retErr = nil; + + NSURL* usersFolderURL = [NSURL fileURLWithPath: @"/Users"]; + NSArray* homeDirectoryURLs = [fileManager contentsOfDirectoryAtURL: usersFolderURL + includingPropertiesForKeys: @[NSURLPathKey, NSURLIsDirectoryKey, NSURLIsReadableKey] + options: NSDirectoryEnumerationSkipsHiddenFiles + error: &retErr]; + if (homeDirectoryURLs == nil || homeDirectoryURLs.count == 0) { + if (retErr != nil) { + return retErr; + } else { + return [SCErr errorWithCode: 700]; + } + } + NSArray* cacheDirPathComponents = @[ + // chrome + @"/Library/Caches/Google/Chrome/Default", + @"/Library/Caches/Google/Chrome/com.google.Chrome", + + // firefox + @"/Library/Caches/Firefox/Profiles", + + // safari + @"/Library/Caches/com.apple.Safari", + @"/Library/Containers/com.apple.Safari/Data/Library/Caches" // this one seems to fail due to permissions issues, but not sure how to fix + ]; + + + NSMutableArray* cacheDirURLs = [NSMutableArray arrayWithCapacity: cacheDirPathComponents.count * homeDirectoryURLs.count]; + for (NSURL* homeDirURL in homeDirectoryURLs) { + for (NSString* cacheDirPathComponent in cacheDirPathComponents) { + [cacheDirURLs addObject: [homeDirURL URLByAppendingPathComponent: cacheDirPathComponent isDirectory: YES]]; + } + } + + for (NSURL* cacheDirURL in cacheDirURLs) { + NSLog(@"Clearing browser cache folder %@", cacheDirURL); + // removeItemAtURL will return errors if the file doesn't exist + // so we don't track the errors - best effort is OK + [fileManager removeItemAtURL: cacheDirURL error: nil]; + } + + return nil; +} + + // migration functions + (NSString*)homeDirectoryForUid:(uid_t)uid { diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 092f4420..d24e8f2c 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -38,7 +38,7 @@ - (void)start { [self.listener resume]; [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { // TODO: DON'T HARDCODE THIS VALUE - [SCDaemonBlockMethods checkupBlockWithControllingUID: 501]; + [SCDaemonBlockMethods checkupBlock]; }]; } diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index 015d203d..407abb37 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate blockSettings:(NSDictionary*)blockSettings authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; -+ (void)checkupBlockWithControllingUID:(uid_t)controllingUID; ++ (void)checkupBlock; + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index cf998b2f..f405ab6b 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -111,7 +111,7 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*) // Clear all caches if the user has the correct preference set, so // that blocked pages are not loaded from a cache. - clearCachesIfRequested(controllingUID); + clearCachesIfRequested(); [SCSentry addBreadcrumb: @"Daemon updated blocklist successfully" category: @"daemon"]; NSLog(@"INFO: Blocklist successfully updated."); @@ -247,7 +247,7 @@ + (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate [self.daemonMethodLock unlock]; } -+ (void)checkupBlockWithControllingUID:(uid_t)controllingUID { ++ (void)checkupBlock { if (![SCDaemonBlockMethods lockOrTimeout: nil timeout: CHECKUP_LOCK_TIMEOUT]) { return; } @@ -261,7 +261,10 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { lastBlockIntegrityCheck = [NSDate distantPast]; } - if(![SCUtilities anyBlockIsRunning: controllingUID]) { + // technically, anyBlockIsRunning without a controllingUID won't find some legacy blocks + // but there should never be a case where the daemon is running with a legacy block, + // so this _should_ be OK. (and it's very annoying to have to pass controllingUID everywhere) + if(![SCUtilities anyBlockIsRunning]) { // No block appears to be running at all in our settings. // Most likely, the user removed it trying to get around the block. Boo! // but for safety and to avoid permablocks (we no longer know when the block should end) @@ -271,7 +274,7 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { [SCSentry captureMessage: @"Checkup ran and no active block found! Removing block, tampering suspected..."]; - removeBlock(controllingUID); + removeBlock(); // Temporarily disabled the TamperingDetection flag because it was sometimes causing false positives // (i.e. people having the background set repeatedly despite no attempts to cheat) @@ -290,7 +293,7 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { if (![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { NSLog(@"INFO: Checkup ran, block expired, removing block."); - removeBlock(controllingUID); + removeBlock(); [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; [SCDaemonUtilities unloadDaemonJob]; @@ -331,7 +334,7 @@ + (void)checkupBlockWithControllingUID:(uid_t)controllingUID { // Perform the re-add of the rules addRulesToFirewall(); - clearCachesIfRequested(controllingUID); + clearCachesIfRequested(); [SCSentry addBreadcrumb: @"Daemon found compromised block integrity and re-added rules" category: @"daemon"]; NSLog(@"INFO: Checkup ran, readded block rules."); diff --git a/SCError.strings b/SCError.strings index c660d614..b6519f17 100644 --- a/SCError.strings +++ b/SCError.strings @@ -41,3 +41,6 @@ // 600-699 = errors generated in SCSettings "600" = "Unable to write SCSettings from a normal (non-root) account."; + +// 700-799 = errors generated in SCUtilities or other utility functions +"700" = "SelfControl couldn't clear your browser caches, because it couldn't read the user home directories."; diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 41dc32e3..1fe7fc0f 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -589,6 +589,8 @@ CB1CA64C25ABA5990084A551 /* Common */ = { isa = PBXGroup; children = ( + CBB1731220F041F4007FCAE9 /* SCUtilities.h */, + CBB1731320F041F4007FCAE9 /* SCUtilities.m */, CBADC27C25B22BC7000EE5BB /* SCSentry.h */, CBADC27D25B22BC7000EE5BB /* SCSentry.m */, CB1465B625B027E700130D2E /* SCErr.h */, @@ -637,8 +639,6 @@ CBEE50C00F48C21F00F5DF1C /* TimerWindowController.m */, CBC2F8650F4674E300CF2A42 /* LaunchctlHelper.h */, CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */, - CBB1731220F041F4007FCAE9 /* SCUtilities.h */, - CBB1731320F041F4007FCAE9 /* SCUtilities.m */, CB5DFCB52251DD1F0084CEC2 /* SCConstants.h */, CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */, F5B8CBEB19EE21340026F3A5 /* Formatters */, From c275dfddb378d39cb70412312dd905ab45e114d4 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 16 Jan 2021 20:37:25 -0800 Subject: [PATCH 46/72] Remove controllingUID from everywhere possible --- AppController.m | 12 ++-- Common/SCUtilities.h | 11 +-- Common/SCUtilities.m | 128 ++++++++++++++++++++++------------ Common/SCXPCAuthorization.m | 4 +- Common/SCXPCClient.h | 4 +- Common/SCXPCClient.m | 8 +-- Daemon/SCDaemonBlockMethods.h | 4 +- Daemon/SCDaemonBlockMethods.m | 17 ++--- Daemon/SCDaemonProtocol.h | 4 +- Daemon/SCDaemonXPC.m | 12 ++-- SCError.strings | 2 + SCKillerHelper/main.m | 4 +- cli-main.m | 4 +- 13 files changed, 124 insertions(+), 90 deletions(-) diff --git a/AppController.m b/AppController.m index a6876b33..91689b96 100755 --- a/AppController.m +++ b/AppController.m @@ -304,7 +304,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { settings_ = [SCSettings sharedSettings]; // go copy over any preferences from legacy setting locations // (we won't clear any old data yet - we leave that to the daemon) - if ([SCUtilities legacySettingsFound]) { + if ([SCUtilities legacySettingsFoundForCurrentUser]) { [SCUtilities copyLegacySettingsToDefaults]; } @@ -640,9 +640,8 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { [self.xpc refreshConnectionAndRun:^{ NSLog(@"Refreshed connection updating active blocklist!"); - [self.xpc updateBlocklistWithControllingUID: getuid() - newBlocklist: [self->defaults_ arrayForKey: @"Blocklist"] - reply:^(NSError * _Nonnull error) { + [self.xpc updateBlocklist: [self->defaults_ arrayForKey: @"Blocklist"] + reply:^(NSError * _Nonnull error) { [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; if (error != nil) { @@ -691,9 +690,8 @@ - (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToA } NSLog(@"Refreshed connection updating active block end date!"); - [self.xpc updateBlockEndDateWithControllingUID: getuid() - newEndDate: newBlockEndDate - reply:^(NSError * _Nonnull error) { + [self.xpc updateBlockEndDate: newBlockEndDate + reply:^(NSError * _Nonnull error) { [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; // let the timer know it needs to recalculate [self->timerWindowController_ performSelectorOnMainThread:@selector(blockEndDateUpdated) diff --git a/Common/SCUtilities.h b/Common/SCUtilities.h index cb9f115a..af19099b 100644 --- a/Common/SCUtilities.h +++ b/Common/SCUtilities.h @@ -35,30 +35,31 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu // Helper tool functions dealing with dictionaries and setDefaultsValue helper // uses the below methods as well as filesystem checks to see if the block is REALLY running or not -+ (BOOL)anyBlockIsRunning:(uid_t)controllingUID; + (BOOL)anyBlockIsRunning; + (BOOL)modernBlockIsRunning; -+ (BOOL)legacyBlockIsRunning:(uid_t)controllingUID; + (BOOL)legacyBlockIsRunning; + (BOOL) blockIsRunningInDictionary:(NSDictionary*)dict; + (BOOL) blockShouldBeRunningInDictionary:(NSDictionary *)dict; ++ (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL; + (BOOL) blockIsRunningInLegacyDictionary:(NSDictionary*)dict; // read and write saved block files + (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; + (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL; ++ (NSArray*)allUserHomeDirectoryURLs:(NSError**)errPtr; + + (NSError*)clearBrowserCaches; // migration methods + (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId; -+ (BOOL)legacySettingsFound:(uid_t)controllingUID; -+ (BOOL)legacySettingsFound; ++ (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID; ++ (BOOL)legacySettingsFoundForCurrentUser; + (NSDate*)legacyBlockEndDate; + (void)copyLegacySettingsToDefaults:(uid_t)controllingUID; + (void)copyLegacySettingsToDefaults; -+ (void)clearLegacySettings:(uid_t)controllingUID; ++ (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID; @end diff --git a/Common/SCUtilities.m b/Common/SCUtilities.m index adc94cc1..e715584e 100644 --- a/Common/SCUtilities.m +++ b/Common/SCUtilities.m @@ -147,8 +147,8 @@ + (NSDictionary*) defaultsDictForUser:(uid_t) controllingUID { return dictValue; } -+ (BOOL)anyBlockIsRunning:(uid_t)controllingUID { - BOOL blockIsRunning = [SCUtilities modernBlockIsRunning] || [self legacyBlockIsRunning: controllingUID]; ++ (BOOL)anyBlockIsRunning { + BOOL blockIsRunning = [SCUtilities modernBlockIsRunning] || [self legacyBlockIsRunning]; // TODO: should this logic be here, or no? // if (!blockIsRunning) { @@ -161,9 +161,6 @@ + (BOOL)anyBlockIsRunning:(uid_t)controllingUID { return blockIsRunning; } -+ (BOOL)anyBlockIsRunning { - return [SCUtilities anyBlockIsRunning: 0]; -} + (BOOL)modernBlockIsRunning { SCSettings* settings = [SCSettings sharedSettings]; @@ -175,30 +172,40 @@ + (BOOL)modernBlockIsRunning { return NO; } -+ (BOOL)legacyBlockIsRunning:(uid_t)controllingUID { ++ (BOOL)legacyBlockIsRunning { // first see if there's a legacy settings file from v3.x - if (!controllingUID) controllingUID = getuid(); - NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: controllingUID]; - NSDictionary* legacySettingsDict = [NSDictionary dictionaryWithContentsOfFile: legacySettingsPath]; - if ([SCUtilities blockIsRunningInLegacyDictionary: legacySettingsDict]) { - return YES; + // which could be in any user's home folder + NSError* homeDirErr = nil; + NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &homeDirErr]; + if (homeDirectoryURLs != nil) { + for (NSURL* homeDirURL in homeDirectoryURLs) { + NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; + NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; + + if ([SCUtilities legacyBlockIsRunningInSettingsFile: settingsFileURL]) { + return YES; + } + } } - + // nope? OK, how about a lock file from pre-3.0? if ([[NSFileManager defaultManager] fileExistsAtPath: SelfControlLegacyLockFilePath]) { return YES; } - // hmm, is there anything in defaults from pre-3.0? - NSDictionary* defaultsDict = [SCUtilities defaultsDictForUser: controllingUID]; - if ([SCUtilities blockIsRunningInLegacyDictionary: defaultsDict]) { - return YES; - } + // we don't check defaults anymore, though pre-3.0 blocks did + // have data stored there. That should be covered by the lockfile anyway return NO; } -+ (BOOL)legacyBlockIsRunning { - return [SCUtilities legacyBlockIsRunning: 0]; + ++ (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL { + NSDictionary* legacySettingsDict = [NSDictionary dictionaryWithContentsOfURL: settingsFileURL]; + + // if the file doesn't exist, there's definitely no block + if (legacySettingsDict == nil) return NO; + + return [SCUtilities blockIsRunningInLegacyDictionary: legacySettingsDict]; } // returns YES if a block is actively running (to the best of our knowledge), and NO otherwise @@ -277,10 +284,9 @@ + (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL { }; } -+ (NSError*)clearBrowserCaches { - NSFileManager* fileManager = [NSFileManager defaultManager]; ++ (NSArray*)allUserHomeDirectoryURLs:(NSError**)errPtr { NSError* retErr = nil; - + NSFileManager* fileManager = [NSFileManager defaultManager]; NSURL* usersFolderURL = [NSURL fileURLWithPath: @"/Users"]; NSArray* homeDirectoryURLs = [fileManager contentsOfDirectoryAtURL: usersFolderURL includingPropertiesForKeys: @[NSURLPathKey, NSURLIsDirectoryKey, NSURLIsReadableKey] @@ -288,11 +294,26 @@ + (NSError*)clearBrowserCaches { error: &retErr]; if (homeDirectoryURLs == nil || homeDirectoryURLs.count == 0) { if (retErr != nil) { - return retErr; + *errPtr = retErr; } else { - return [SCErr errorWithCode: 700]; + *errPtr = [SCErr errorWithCode: 700]; } + + [SCSentry captureError: *errPtr]; + + return nil; } + + return homeDirectoryURLs; +} + ++ (NSError*)clearBrowserCaches { + NSFileManager* fileManager = [NSFileManager defaultManager]; + + NSError* homeDirErr = nil; + NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &homeDirErr]; + if (homeDirectoryURLs == nil) return homeDirErr; + NSArray* cacheDirPathComponents = @[ // chrome @"/Library/Caches/Google/Chrome/Default", @@ -339,8 +360,7 @@ + (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId { // check all legacy settings (old secured settings, lockfile, old-school defaults) // to see if there's anything there -+ (BOOL)legacySettingsFound:(uid_t)controllingUID { - if (!controllingUID) controllingUID = getuid(); ++ (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID { NSFileManager* fileMan = [NSFileManager defaultManager]; NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: controllingUID]; NSArray* defaultsHostBlacklist; @@ -356,13 +376,13 @@ + (BOOL)legacySettingsFound:(uid_t)controllingUID { return defaultsHostBlacklist || [fileMan fileExistsAtPath: legacySettingsPath] || [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]; } -+ (BOOL)legacySettingsFound { - return [SCUtilities legacySettingsFound: 0]; ++ (BOOL)legacySettingsFoundForCurrentUser { + return [SCUtilities legacySettingsFoundForUser: getuid()]; } + (NSDate*)legacyBlockEndDate { // if we're running this as a normal user (generally that means app/CLI), it's easy: just get the standard user defaults - // if we're running this as root, we need to be given a UID target, then we imitate them to grab their defaults + // this method can't be run as root, it won't work NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; NSDictionary* lockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLegacyLockFilePath]; NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: getuid()]; @@ -480,7 +500,7 @@ + (void)copyLegacySettingsToDefaults { // - the defaults system // - a v3.x per-user secured settings file // we should check for block settings in all of these places and get rid of them -+ (void)clearLegacySettings:(uid_t)controllingUID { ++ (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID { NSLog(@"Clearing legacy settings!"); BOOL runningAsRoot = (geteuid() == 0); @@ -488,15 +508,21 @@ + (void)clearLegacySettings:(uid_t)controllingUID { // if we're not running as root, or we didn't get a valid non-root controlling UID // we won't have permissions to make this work. This method MUST be called with root perms NSLog(@"ERROR: Can't clear legacy settings, because we aren't running as root."); - return; + NSError* err = [SCErr errorWithCode: 701]; + [SCSentry captureError: err]; + return err; } // if we're gonna clear settings, there can't be a block running anywhere. otherwise, we should wait! - if ([SCUtilities legacyBlockIsRunning: controllingUID]) { + if ([SCUtilities legacyBlockIsRunning]) { NSLog(@"ERROR: Can't clear legacy settings because a block is ongoing!"); - return; + NSError* err = [SCErr errorWithCode: 702]; + [SCSentry captureError: err]; + return err; } - + + NSFileManager* fileMan = [NSFileManager defaultManager]; + // besides Blocklist and the values copied from the v3.0-3.0.3 settings file to defaults in copyLegacySettingsToDefaults // we actually don't need to move anything else over! Why? // 1. The other settings from 3.0-3.0.3 don't matter as long as a block isn't running (i.e. BlockIsRunning should be false @@ -504,11 +530,14 @@ + (void)clearLegacySettings:(uid_t)controllingUID { // 2. All of the non-block settings from pre-3.0 can stay in defaults, ahd BlockStartedDate should be false if no block running // so all that's left is to clear out the legacy crap for good + // if an error happens trying to clear any portion of the old settings, + // we'll remember it, log it, and return it, but still try to clear the rest (best-effort) + NSError* retErr = nil; + // first, clear the pre-3.0 lock dictionary - NSError* removeLockFileErr; - NSFileManager* fileMan = [NSFileManager defaultManager]; - if(![fileMan removeItemAtPath: SelfControlLegacyLockFilePath error: &removeLockFileErr] && [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]) { - NSLog(@"WARNING: Could not remove legacy SelfControl lock file because of error: %@", removeLockFileErr); + if(![fileMan removeItemAtPath: SelfControlLegacyLockFilePath error: &retErr] && [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]) { + NSLog(@"WARNING: Could not remove legacy SelfControl lock file because of error: %@", retErr); + [SCSentry captureError: retErr]; } // then, clear keys out of defaults which aren't used @@ -517,7 +546,6 @@ + (void)clearLegacySettings:(uid_t)controllingUID { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; [defaults addSuiteNamed: @"org.eyebeam.SelfControl"]; [defaults synchronize]; - NSArray* defaultsKeysToClear = @[ @"BlockStartedDate", @"BlockEndDate", @@ -526,21 +554,29 @@ + (void)clearLegacySettings:(uid_t)controllingUID { for (NSString* key in defaultsKeysToClear) { [defaults removeObjectForKey: key]; } - [defaults synchronize]; [NSUserDefaults resetStandardUserDefaults]; seteuid(0); - // finally, clear the old settings file if they have it from a v3.0-3.0.3 version - NSError* removeOldSettingsFileErr; - NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: controllingUID]; - if(![fileMan removeItemAtPath: legacySettingsPath error: &removeOldSettingsFileErr] && [fileMan fileExistsAtPath: legacySettingsPath]) { - NSLog(@"WARNING: Could not remove legacy SelfControl lock file because of error: %@", removeOldSettingsFileErr); + // clear all legacy per-user secured settings (v3.0-3.0.3) in every user's home folder + NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &retErr]; + if (homeDirectoryURLs != nil) { + for (NSURL* homeDirURL in homeDirectoryURLs) { + NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; + NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; + + if(![fileMan removeItemAtURL: settingsFileURL error: &retErr] && [fileMan fileExistsAtPath: settingsFileURL.path]) { + NSLog(@"WARNING: Could not remove legacy SelfControl settings file at URL %@ because of error: %@", settingsFileURL, retErr); + [SCSentry captureError: retErr]; + } + } } - + // and that's it! note that we don't touch the modern SCSettings at all, and that's OK - it'll restart from scratch and be fine [SCSentry addBreadcrumb: @"Cleared legacy settings successfully" category: @"settings"]; NSLog(@"Cleared legacy settings!"); + + return retErr; } diff --git a/Common/SCXPCAuthorization.m b/Common/SCXPCAuthorization.m index 8fb1e508..3f594f07 100644 --- a/Common/SCXPCAuthorization.m +++ b/Common/SCXPCAuthorization.m @@ -101,7 +101,7 @@ + (NSDictionary *)commandInfo @"prompt shown when user is required to authorize to start block" ) }, - NSStringFromSelector(@selector(updateBlocklistWithControllingUID:newBlocklist:authorization:reply:)) : @{ + NSStringFromSelector(@selector(updateBlocklist:authorization:reply:)) : @{ kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, kCommandKeyAuthRightDesc : NSLocalizedString( @@ -109,7 +109,7 @@ + (NSDictionary *)commandInfo @"prompt shown when user is required to authorize to add to their blocklist" ) }, - NSStringFromSelector(@selector(updateBlockEndDateWithControllingUID:newEndDate:authorization:reply:)) : @{ + NSStringFromSelector(@selector(updateBlockEndDate:authorization:reply:)) : @{ kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, kCommandKeyAuthRightDesc : NSLocalizedString( diff --git a/Common/SCXPCClient.h b/Common/SCXPCClient.h index 0b15db15..dd9f06c3 100644 --- a/Common/SCXPCClient.h +++ b/Common/SCXPCClient.h @@ -20,8 +20,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)getVersion:(void(^)(NSString* version, NSError* error))reply; - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate blockSettings:(NSDictionary*)blockSettings reply:(void(^)(NSError* error))reply; -- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply; -- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))reply; +- (void)updateBlocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply; +- (void)updateBlockEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))reply; @end diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index 975a3e56..c4480e0e 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -254,7 +254,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply { +- (void)updateBlocklist:(NSArray*)newBlocklist reply:(void(^)(NSError* error))reply { [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Blocklist update failed with connection error: %@", connectError); @@ -265,7 +265,7 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS NSLog(@"Blocklist update command failed with remote object proxy error: %@", proxyError); [SCSentry captureError: proxyError]; reply(proxyError); - }] updateBlocklistWithControllingUID: controllingUID newBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { + }] updateBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { if (error != nil) { NSLog(@"Blocklist update failed with error = %@\n", error); [SCSentry captureError: error]; @@ -276,7 +276,7 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS }]; } -- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))reply { +- (void)updateBlockEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))reply { [self connectAndExecuteCommandBlock:^(NSError * connectError) { if (connectError != nil) { NSLog(@"Block end date update failed with connection error: %@", connectError); @@ -287,7 +287,7 @@ - (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(N NSLog(@"Block end date update command failed with remote object proxy error: %@", proxyError); [SCSentry captureError: proxyError]; reply(proxyError); - }] updateBlockEndDateWithControllingUID: controllingUID newEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { + }] updateBlockEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { if (error != nil) { NSLog(@"Block end date update failed with error = %@\n", error); [SCSentry captureError: error]; diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index 407abb37..f16b7b3c 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -17,9 +17,9 @@ NS_ASSUME_NONNULL_BEGIN + (void)checkupBlock; -+ (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; ++ (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; -+ (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; ++ (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; @end diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index f405ab6b..363808cf 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -53,7 +53,7 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { ++ (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { if (![SCDaemonBlockMethods lockOrTimeout: reply]) { return; } [SCSentry addBreadcrumb: @"Daemon method updateBlocklist called" category: @"daemon"]; - if ([SCUtilities legacyBlockIsRunning: controllingUID]) { + if ([SCUtilities legacyBlockIsRunning]) { NSLog(@"ERROR: Can't update blocklist because a legacy block is running"); NSError* err = [SCErr errorWithCode: 303]; [SCSentry captureError: err]; @@ -190,14 +190,14 @@ + (void)updateBlocklist:(uid_t)controllingUID newBlocklist:(NSArray*) [self.daemonMethodLock unlock]; } -+ (void)updateBlockEndDate:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { ++ (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { if (![SCDaemonBlockMethods lockOrTimeout: reply]) { return; } [SCSentry addBreadcrumb: @"Daemon method updateBlockEndDate called" category: @"daemon"]; - if ([SCUtilities legacyBlockIsRunning: controllingUID]) { + if ([SCUtilities legacyBlockIsRunning]) { NSLog(@"ERROR: Can't update block end date because a legacy block is running"); NSError* err = [SCErr errorWithCode: 306]; [SCSentry captureError: err]; @@ -261,9 +261,6 @@ + (void)checkupBlock { lastBlockIntegrityCheck = [NSDate distantPast]; } - // technically, anyBlockIsRunning without a controllingUID won't find some legacy blocks - // but there should never be a case where the daemon is running with a legacy block, - // so this _should_ be OK. (and it's very annoying to have to pass controllingUID everywhere) if(![SCUtilities anyBlockIsRunning]) { // No block appears to be running at all in our settings. // Most likely, the user removed it trying to get around the block. Boo! diff --git a/Daemon/SCDaemonProtocol.h b/Daemon/SCDaemonProtocol.h index c58a8b52..8e5261c6 100644 --- a/Daemon/SCDaemonProtocol.h +++ b/Daemon/SCDaemonProtocol.h @@ -13,9 +13,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate blockSettings:(NSDictionary*)blockSettings authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; -- (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; -- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +- (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; - (BOOL) checkup; diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 2981b5d1..5e621d6e 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -27,8 +27,8 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { - NSLog(@"XPC method called: updateBlocklistWithControllingUID"); +- (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"XPC method called: updateBlocklist"); NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; if (error != nil) { @@ -40,11 +40,11 @@ - (void)updateBlocklistWithControllingUID:(uid_t)controllingUID newBlocklist:(NS NSLog(@"AUTHORIZATION ACCEPTED for updateBlocklist with authData %@ and command %s", authData, sel_getName(_cmd)); } - [SCDaemonBlockMethods updateBlocklist: controllingUID newBlocklist: newBlocklist authorization: authData reply: reply]; + [SCDaemonBlockMethods updateBlocklist: newBlocklist authorization: authData reply: reply]; } -- (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { - NSLog(@"XPC method called: updateBlockEndDateWithControllingUID"); +- (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply { + NSLog(@"XPC method called: updateBlockEndDate"); NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; if (error != nil) { @@ -56,7 +56,7 @@ - (void)updateBlockEndDateWithControllingUID:(uid_t)controllingUID newEndDate:(N NSLog(@"AUTHORIZATION ACCEPTED for updateBlockENdDate with authData %@ and command %s", authData, sel_getName(_cmd)); } - [SCDaemonBlockMethods updateBlockEndDate:controllingUID newEndDate: newEndDate authorization: authData reply: reply]; + [SCDaemonBlockMethods updateBlockEndDate: newEndDate authorization: authData reply: reply]; } - (BOOL) checkup { diff --git a/SCError.strings b/SCError.strings index b6519f17..8254192f 100644 --- a/SCError.strings +++ b/SCError.strings @@ -44,3 +44,5 @@ // 700-799 = errors generated in SCUtilities or other utility functions "700" = "SelfControl couldn't clear your browser caches, because it couldn't read the user home directories."; +"701" = "SelfControl couldn't clear out your settings from old version of SelfControl, because we had insufficient permissions."; +"702" = "SelfControl couldn't clear out your settings from old version of SelfControl, because a block from an older version is ongoing."; diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index 0c969e4f..bea5da61 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -166,9 +166,9 @@ int main(int argc, char* argv[]) { [settings synchronizeSettings]; [log appendFormat: @"Reset all modern secured settings to default values.\n"]; - if ([SCUtilities legacySettingsFound: controllingUID]) { + if ([SCUtilities legacySettingsFoundForUser: controllingUID]) { [SCUtilities copyLegacySettingsToDefaults: controllingUID]; - [SCUtilities clearLegacySettings: controllingUID]; + [SCUtilities clearLegacySettingsForUser: controllingUID]; [log appendFormat: @"Found, copied, and cleared legacy settings (v3.0-3.0.3)!\n"]; } else { [log appendFormat: @"No legacy settings (v3.0-3.0.3) found.\n"]; diff --git a/cli-main.m b/cli-main.m index 0b217900..702bd4ba 100755 --- a/cli-main.m +++ b/cli-main.m @@ -56,7 +56,7 @@ int main(int argc, char* argv[]) { if([modeString isEqual: @"--install"]) { [SCSentry addBreadcrumb: @"CLI method --install called" category: @"cli"]; - if ([SCUtilities anyBlockIsRunning: controllingUID]) { + if ([SCUtilities anyBlockIsRunning]) { NSLog(@"ERROR: Block is already running"); exit(EX_CONFIG); } @@ -187,7 +187,7 @@ int main(int argc, char* argv[]) { NSLog(@"%@", [settings dictionaryRepresentation]); } else if ([modeString isEqualToString: @"--is-running"]) { [SCSentry addBreadcrumb: @"CLI method --is-running called" category: @"cli"]; - BOOL blockIsRunning = [SCUtilities anyBlockIsRunning: controllingUID]; + BOOL blockIsRunning = [SCUtilities anyBlockIsRunning]; NSLog(@"%@", blockIsRunning ? @"YES" : @"NO"); } else if ([modeString isEqualToString: @"--version"]) { [SCSentry addBreadcrumb: @"CLI method --version called" category: @"cli"]; From b9d431e47026b0280a66cb1388578083506e73d1 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 16 Jan 2021 21:02:22 -0800 Subject: [PATCH 47/72] Check host file in app blockIsRunning --- AppController.m | 15 ++++++++++---- Common/HelperCommon.m | 8 ++++++-- Common/SCSettings.m | 29 ++++++++++++++------------- Common/SCUtilities.m | 9 --------- Daemon/SCDaemon.m | 1 - Daemon/SCDaemonBlockMethods.m | 8 +++++--- HostFileBlocker.h | 2 ++ HostFileBlocker.m | 10 ++++++++++ SCKillerHelper/main.m | 37 +++++++++++++---------------------- TimerWindowController.m | 25 +++++++++++------------ cli-main.m | 8 ++------ 11 files changed, 76 insertions(+), 76 deletions(-) diff --git a/AppController.m b/AppController.m index 91689b96..27f174f4 100755 --- a/AppController.m +++ b/AppController.m @@ -32,6 +32,7 @@ #import #import "SCXPCClient.h" #import +#import "HostFileBlocker.h" @interface AppController () {} @@ -411,7 +412,11 @@ - (void)reinstallDaemon { } - (BOOL)blockIsRunning { - return [SCUtilities anyBlockIsRunning]; + // we'll say a block is running if we find the block info, but + // also, importantly, if we find a block still going in the hosts file + // that way if this happens, the user will still see the timer window - + // which will let them manually clear the remaining block info after 10 seconds + return [SCUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]; } - (IBAction)showDomainList:(id)sender { @@ -515,9 +520,11 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { - (void)extendBlockTime:(NSInteger)minutesToAdd lock:(NSLock*)lock { // sanity check: extending a block for 0 minutes is useless; 24 hour should be impossible - NSInteger MINUTES_IN_DAY = 24 * 60 * 60; - if(minutesToAdd < 1 || minutesToAdd > MINUTES_IN_DAY) - return; + NSInteger maxBlockLength = [defaults_ integerForKey: @"MaxBlockLength"]; + if(minutesToAdd < 1) return; + if (minutesToAdd > maxBlockLength) { + minutesToAdd = maxBlockLength; + } // ensure block health before we try to change it if(![self blockIsRunning]) { diff --git a/Common/HelperCommon.m b/Common/HelperCommon.m index b17ea442..8fbc3e81 100644 --- a/Common/HelperCommon.m +++ b/Common/HelperCommon.m @@ -106,8 +106,12 @@ void clearCachesIfRequested() { return; } - // TODO: do something with the NSError returned by this method - [SCUtilities clearBrowserCaches]; + NSError* err = [SCUtilities clearBrowserCaches]; + if (err) { + NSLog(@"WARNING: Error clearing browser caches: %@", err); + [SCSentry captureError: err]; + } + clearOSDNSCache(); } diff --git a/Common/SCSettings.m b/Common/SCSettings.m index 2c8a564d..e57fd6e6 100644 --- a/Common/SCSettings.m +++ b/Common/SCSettings.m @@ -21,6 +21,7 @@ @interface SCSettings () @property (readonly) NSMutableDictionary* settingsDict; @property NSDate* lastSynchronizedWithDisk; @property dispatch_source_t syncTimer; +@property dispatch_source_t debouncedChangeTimer; @end @@ -461,14 +462,16 @@ - (void)startSyncTimer { dispatch_resume(self.syncTimer); } } -- (void)cancelSyncTimer { - if (self.syncTimer == nil) { - // no active timer, no need to cancel - return; +- (void)cancelSyncTimers { + if (self.syncTimer != nil) { + dispatch_source_cancel(self.syncTimer); + self.syncTimer = nil; + } + + if (self.debouncedChangeTimer != nil) { + dispatch_source_cancel(self.debouncedChangeTimer); + self.debouncedChangeTimer = nil; } - - dispatch_source_cancel(self.syncTimer); - self.syncTimer = nil; } - (void)updateSentryContext { @@ -544,14 +547,13 @@ - (void)onSettingChanged:(NSNotification*)note { // regardless of which is more recent, we should really go get the new deal from disk // in the near future (but debounce so we don't do this a million times for rapid changes) - static dispatch_source_t debouncedSyncTimer = nil; - if (debouncedSyncTimer != nil) { - dispatch_source_cancel(debouncedSyncTimer); - debouncedSyncTimer = nil; + if (self.debouncedChangeTimer != nil) { + dispatch_source_cancel(self.debouncedChangeTimer); + self.debouncedChangeTimer = nil; } dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); double throttleSecs = 0.25f; - debouncedSyncTimer = CreateDebounceDispatchTimer(throttleSecs, queue, ^{ + self.debouncedChangeTimer = CreateDebounceDispatchTimer(throttleSecs, queue, ^{ NSLog(@"Syncing settings due to propagated changes"); [self synchronizeSettings]; }); @@ -572,8 +574,7 @@ - (void)resetAllSettingsToDefaults { } - (void)dealloc { - // TODO: should we kill the debounced timer above also? - [self cancelSyncTimer]; + [self cancelSyncTimers]; } @synthesize settingsDict = _settingsDict, lastSynchronizedWithDisk; diff --git a/Common/SCUtilities.m b/Common/SCUtilities.m index e715584e..778157b8 100644 --- a/Common/SCUtilities.m +++ b/Common/SCUtilities.m @@ -149,15 +149,6 @@ + (NSDictionary*) defaultsDictForUser:(uid_t) controllingUID { + (BOOL)anyBlockIsRunning { BOOL blockIsRunning = [SCUtilities modernBlockIsRunning] || [self legacyBlockIsRunning]; - - // TODO: should this logic be here, or no? -// if (!blockIsRunning) { -// // last try if we can't find a block anywhere: check the host file, and see if a block is in there -// NSString* hostFileContents = [NSString stringWithContentsOfFile: @"/etc/hosts" encoding: NSUTF8StringEncoding error: NULL]; -// if(hostFileContents != nil && [hostFileContents rangeOfString: @"# BEGIN SELFCONTROL BLOCK"].location != NSNotFound) { -// blockIsRunning = YES; -// } -// } return blockIsRunning; } diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index d24e8f2c..6a401f59 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -37,7 +37,6 @@ - (id) init { - (void)start { [self.listener resume]; [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { - // TODO: DON'T HARDCODE THIS VALUE [SCDaemonBlockMethods checkupBlock]; }]; } diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 363808cf..58f22427 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -106,7 +106,6 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData [settings setValue: newBlocklist forKey: @"ActiveBlocklist"]; [settings synchronizeSettings]; // make sure everyone knows about our new list - // TODO: is this still necessary in the new daemon world? sendConfigurationChangedNotification(); // Clear all caches if the user has the correct preference set, so @@ -238,7 +236,6 @@ + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData [settings setValue: newEndDate forKey: @"BlockEndDate"]; [settings synchronizeSettings]; // make sure everyone knows about our new end date - // TODO: is this still necessary in the new daemon world? sendConfigurationChangedNotification(); [SCSentry addBreadcrumb: @"Daemon extended block successfully" category: @"daemon"]; @@ -273,6 +270,8 @@ + (void)checkupBlock { removeBlock(); + sendConfigurationChangedNotification(); + // Temporarily disabled the TamperingDetection flag because it was sometimes causing false positives // (i.e. people having the background set repeatedly despite no attempts to cheat) // We will try to bring this feature back once we can debug it @@ -291,6 +290,9 @@ + (void)checkupBlock { NSLog(@"INFO: Checkup ran, block expired, removing block."); removeBlock(); + + sendConfigurationChangedNotification(); + [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; [SCDaemonUtilities unloadDaemonJob]; diff --git a/HostFileBlocker.h b/HostFileBlocker.h index 11c39129..7f2788ec 100755 --- a/HostFileBlocker.h +++ b/HostFileBlocker.h @@ -30,6 +30,8 @@ NSFileManager* fileMan; } ++ (BOOL)blockFoundInHostsFile; + - (BOOL)deleteBackupHostsFile; - (void)revertFileContentsToDisk; diff --git a/HostFileBlocker.m b/HostFileBlocker.m index 7bb03e2b..0c8b2aec 100755 --- a/HostFileBlocker.m +++ b/HostFileBlocker.m @@ -52,6 +52,16 @@ - (HostFileBlocker*)init { return self; } ++ (BOOL)blockFoundInHostsFile { + // last try if we can't find a block anywhere: check the host file, and see if a block is in there + NSString* hostFileContents = [NSString stringWithContentsOfFile: kHostFileBlockerPath encoding: NSUTF8StringEncoding error: NULL]; + if(hostFileContents != nil && [hostFileContents rangeOfString: kHostFileBlockerSelfControlHeader].location != NSNotFound) { + return YES; + } + + return NO; +} + - (void)revertFileContentsToDisk { [strLock lock]; diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index bea5da61..7f20db39 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -219,33 +219,24 @@ int main(int argc, char* argv[]) { } // OK, make sure all settings are synced before this thing exits - [settings synchronizeSettingsWithCompletion:^(NSError* err) { - if (err != nil) { - [log appendFormat: @"\nWARNING: Settings failed to synchronize before exit, with error %@", err]; - } + NSError* syncSettingsErr = nil; + [settings syncSettingsAndWait: 5 error: &syncSettingsErr]; + + if (syncSettingsErr != nil) { + [log appendFormat: @"\nWARNING: Settings failed to synchronize before exit, with error %@", syncSettingsErr]; + } - // let the main app know to refresh - sendConfigurationChangedNotification(); + [log appendString: @"\n===SelfControl-Killer complete!==="]; - [log appendString: @"\n===SelfControl-Killer complete!==="]; + [log writeToFile: logFilePath + atomically: YES + encoding: NSUTF8StringEncoding + error: nil]; - [log writeToFile: logFilePath - atomically: YES - encoding: NSUTF8StringEncoding - error: nil]; - exit(EX_OK); - }]; - - // only wait 5 seconds for the sync to finish, otherwise exit anyway - sleep(5); - - [log appendString: @"\nWARNING: Settings timed out synchronizing before exit"]; - [log writeToFile: logFilePath - atomically: YES - encoding: NSUTF8StringEncoding - error: nil]; - + // let the main app know to refresh + sendConfigurationChangedNotification(); + exit(EX_OK); } } diff --git a/TimerWindowController.m b/TimerWindowController.m index 2746e0f9..a6599e07 100755 --- a/TimerWindowController.m +++ b/TimerWindowController.m @@ -112,23 +112,20 @@ - (void)awakeFromNib { - (void)blockEnded { - if(![SCUtilities anyBlockIsRunning]) { - [timerUpdater_ invalidate]; - timerUpdater_ = nil; + [timerUpdater_ invalidate]; + timerUpdater_ = nil; - [timerLabel_ setStringValue: NSLocalizedString(@"Block not active", @"block not active string")]; - [timerLabel_ setFont: [[NSFontManager sharedFontManager] - convertFont: [timerLabel_ font] - toSize: 37] - ]; + [timerLabel_ setStringValue: NSLocalizedString(@"Block not active", @"block not active string")]; + [timerLabel_ setFont: [[NSFontManager sharedFontManager] + convertFont: [timerLabel_ font] + toSize: 37] + ]; - [timerLabel_ sizeToFit]; + [timerLabel_ sizeToFit]; - [self resetStrikes]; - - [SCSentry addBreadcrumb: @"Block ended and timer window is closing" category: @"app"]; - - } + [self resetStrikes]; + + [SCSentry addBreadcrumb: @"Block ended and timer window is closing" category: @"app"]; } diff --git a/cli-main.m b/cli-main.m index 702bd4ba..457783bf 100755 --- a/cli-main.m +++ b/cli-main.m @@ -27,8 +27,7 @@ #import "SCXPCClient.h" // The main method which deals which most of the logic flow and execution of -// the CLI helper tool. Posts an SCConfigurationChangedNotification if the block -// is enabled or disabled. +// the CLI tool. int main(int argc, char* argv[]) { [SCSentry startSentry: @"org.eyebeam.selfcontrol-cli"]; @@ -155,10 +154,7 @@ int main(int argc, char* argv[]) { exit(EX_SOFTWARE); return; } - - // TODO: is this necessary? - sendConfigurationChangedNotification(); - + NSLog(@"INFO: Block successfully added."); dispatch_semaphore_signal(installingBlockSema); }]; From be5ecdd1a3eff79b8159af4ee52939357fb63428 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 16 Jan 2021 21:03:21 -0800 Subject: [PATCH 48/72] minor fix squash --- SelfControl.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 1fe7fc0f..506f886d 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -48,6 +48,8 @@ CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */ = {isa = PBXBuildFile; fileRef = CB529BBE0F32B7ED00564FB8 /* AppController.m */; }; CB54D44C0F93E33300AA22E9 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E901D0F397FFA006DE6E4 /* Security.framework */; }; CB587E500F50FE8800C66A09 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB587E4F0F50FE8800C66A09 /* SystemConfiguration.framework */; }; + CB58948025B3FC6D00E9A5C0 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; + CB58948725B3FC6F00E9A5C0 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; CB5DFCB72251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB5DFCB82251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB5DFCBA2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; @@ -1221,6 +1223,7 @@ CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */, CB1465B825B027E700130D2E /* SCErr.m in Sources */, CBB1731920F05C07007FCAE9 /* SCUtilities.m in Sources */, + CB58948025B3FC6D00E9A5C0 /* HostFileBlocker.m in Sources */, F5B8CBEE19EE21C30026F3A5 /* SCTimeIntervalFormatter.m in Sources */, CBD4848A19D764440020F949 /* PreferencesGeneralViewController.m in Sources */, CB1CA65E25ABA6150084A551 /* SCXPCClient.m in Sources */, @@ -1283,6 +1286,7 @@ CB9C80FC19CFB79700CDCAE1 /* main.m in Sources */, CB5DFCBA2251DD1F0084CEC2 /* SCConstants.m in Sources */, CB32D2AC21902CF800B8CD68 /* SCSettings.m in Sources */, + CB58948725B3FC6F00E9A5C0 /* HostFileBlocker.m in Sources */, CB1465BA25B027E700130D2E /* SCErr.m in Sources */, CBB1731C20F05C0A007FCAE9 /* SCUtilities.m in Sources */, CBADC28025B22BC7000EE5BB /* SCSentry.m in Sources */, From 7ade1d32f3e4e623576432571a2d874699db3bb0 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sat, 16 Jan 2021 21:39:03 -0800 Subject: [PATCH 49/72] Don't show an error if the user cancels authorization --- AppController.m | 40 ++++++++++++++++++++------------ Common/SCUtilities.h | 2 ++ Common/SCUtilities.m | 14 +++++++++++ Common/SCXPCClient.m | 33 +++++++++++++++++--------- Daemon/SCDaemonXPC.m | 19 ++++++++++----- SCError.strings | 3 ++- SelfControl Killer/AppDelegate.m | 17 ++++++++++---- TimerWindowController.m | 7 ++++-- 8 files changed, 95 insertions(+), 40 deletions(-) diff --git a/AppController.m b/AppController.m index 27f174f4..4dbae646 100755 --- a/AppController.m +++ b/AppController.m @@ -405,8 +405,10 @@ - (void)reinstallDaemon { NSLog(@"Retrying helper tool connection..."); [self.xpc performSelectorOnMainThread: @selector(connectToHelperTool) withObject: nil waitUntilDone: YES]; } else { - NSLog(@"ERROR: Reinstalling daemon failed with error %@", error); - [NSApp presentError: error]; + if (![SCUtilities errorIsAuthCanceled: error]) { + NSLog(@"ERROR: Reinstalling daemon failed with error %@", error); + [NSApp presentError: error]; + } } }]; } @@ -578,9 +580,11 @@ - (void)installBlock { [self refreshUserInterface]; [self.xpc installDaemon:^(NSError * _Nonnull error) { if (error != nil) { - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: error - waitUntilDone: YES]; + if (![SCUtilities errorIsAuthCanceled: error]) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } self.addingBlock = false; [self refreshUserInterface]; return; @@ -614,10 +618,12 @@ - (void)installBlock { } reply:^(NSError * _Nonnull error) { NSLog(@"WOO started block with error %@", error); - if (error != nil) { - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: error - waitUntilDone: YES]; + if (error != nil ) { + if (![SCUtilities errorIsAuthCanceled: error]) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } } else { [SCSentry addBreadcrumb: @"Block started successfully" category:@"app"]; } @@ -652,9 +658,11 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; if (error != nil) { - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: error - waitUntilDone: YES]; + if (![SCUtilities errorIsAuthCanceled: error]) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } } else { [SCSentry addBreadcrumb: @"Blocklist updated successfully" category:@"app"]; } @@ -706,9 +714,11 @@ - (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToA waitUntilDone: YES]; if (error != nil) { - [NSApp performSelectorOnMainThread: @selector(presentError:) - withObject: error - waitUntilDone: YES]; + if (![SCUtilities errorIsAuthCanceled: error]) { + [NSApp performSelectorOnMainThread: @selector(presentError:) + withObject: error + waitUntilDone: YES]; + } } else { [SCSentry addBreadcrumb: @"App extended block duration successfully" category:@"app"]; } diff --git a/Common/SCUtilities.h b/Common/SCUtilities.h index af19099b..fe70d8e2 100644 --- a/Common/SCUtilities.h +++ b/Common/SCUtilities.h @@ -53,6 +53,8 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu + (NSError*)clearBrowserCaches; ++ (BOOL)errorIsAuthCanceled:(NSError*)err; + // migration methods + (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId; + (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID; diff --git a/Common/SCUtilities.m b/Common/SCUtilities.m index 778157b8..b6275dc5 100644 --- a/Common/SCUtilities.m +++ b/Common/SCUtilities.m @@ -238,6 +238,20 @@ + (BOOL)blockIsRunningInLegacyDictionary:(NSDictionary*)dict { } } + ++ (BOOL)errorIsAuthCanceled:(NSError*)err { + if (err == nil) return NO; + + if ([err.domain isEqualToString: NSOSStatusErrorDomain] && err.code == -60006) { + return YES; + } + if (err.domain == kSelfControlErrorDomain && err.code == 1) { + return YES; + } + + return NO; +} + + (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef { NSDictionary* saveDict = @{@"HostBlacklist": [blockInfo objectForKey: @"Blocklist"], @"BlockAsWhitelist": [blockInfo objectForKey: @"BlockAsWhitelist"]}; diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index c4480e0e..84fdb1a6 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -143,10 +143,19 @@ - (void)installDaemon:(void(^)(NSError*))callback { &authorizationRef); if(status) { - NSLog(@"ERROR: Failed to authorize installing selfcontrold."); - NSError* err = [SCErr errorWithCode: 501]; - // this usually just means the user clicked Cancel, so don't report to Sentry - callback(err); + // if it's just the user cancelling, make that obvious + // to any listeners so they can ignore it appropriately + if (status == -60006) { + callback([SCErr errorWithCode: 1]); + } else { + NSLog(@"ERROR: Failed to authorize installing selfcontrold with status %d.", status); + + NSError* err = [SCErr errorWithCode: 501]; + [SCSentry captureError: err]; + + callback(err); + } + return; } @@ -163,7 +172,9 @@ - (void)installDaemon:(void(^)(NSError*))callback { NSLog(@"WARNING: Authorized installation of selfcontrold returned failure status code %d and error %@", (int)status, error); NSError* err = [SCErr errorWithCode: 500 subDescription: error.localizedDescription]; - [SCSentry captureError: err]; + if (![SCUtilities errorIsAuthCanceled: error]) { + [SCSentry captureError: err]; + } callback(err); return; @@ -236,16 +247,16 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist reply:(void(^)(NSError [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { - if (error != nil) { + if (error != nil && ![SCUtilities errorIsAuthCanceled: error]) { NSLog(@"Blocklist update failed with error = %@\n", error); [SCSentry captureError: error]; } @@ -288,7 +299,7 @@ - (void)updateBlockEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))re [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlockEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { - if (error != nil) { + if (error != nil && ![SCUtilities errorIsAuthCanceled: error]) { NSLog(@"Block end date update failed with error = %@\n", error); [SCSentry captureError: error]; } diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 5e621d6e..e0f31320 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -8,6 +8,7 @@ #import "SCDaemonXPC.h" #import "SCDaemonBlockMethods.h" #import "SCXPCAuthorization.h" +#import "SCUtilities.h" @implementation SCDaemonXPC @@ -16,8 +17,10 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; if (error != nil) { - NSLog(@"ERROR: XPC authorization failed due to error %@", error); - [SCSentry captureError: error]; + if (![SCUtilities errorIsAuthCanceled: error]) { + NSLog(@"ERROR: XPC authorization failed due to error %@", error); + [SCSentry captureError: error]; + } reply(error); return; } else { @@ -48,8 +53,10 @@ - (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; if (error != nil) { - NSLog(@"ERROR: XPC authorization failed due to error %@", error); - [SCSentry captureError: error]; + if (![SCUtilities errorIsAuthCanceled: error]) { + NSLog(@"ERROR: XPC authorization failed due to error %@", error); + [SCSentry captureError: error]; + } reply(error); return; } else { diff --git a/SCError.strings b/SCError.strings index 8254192f..9510cda9 100644 --- a/SCError.strings +++ b/SCError.strings @@ -6,7 +6,8 @@ */ -// 0-99 = general / generic errors +// 1-99 = general / generic errors +"1" = "Authorization cancelled"; // 100 - 199 = errors generated in the app "100" = "You can't start block, because the blocklist is empty."; diff --git a/SelfControl Killer/AppDelegate.m b/SelfControl Killer/AppDelegate.m index ed7e6d79..6d7fc739 100644 --- a/SelfControl Killer/AppDelegate.m +++ b/SelfControl Killer/AppDelegate.m @@ -47,8 +47,12 @@ - (IBAction)killButtonClicked:(id)sender { &authorizationRef); if(status) { - NSLog(@"ERROR: Failed to authorize block kill."); - return; + // if it's just the user cancelling, make that obvious + // to any listeners so they can ignore it appropriately + if (status != -60006) { + NSLog(@"ERROR: Failed to authorize block kill with status %d.", status); + } + return; } // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid @@ -67,10 +71,13 @@ - (IBAction)killButtonClicked:(id)sender { if(status) { NSLog(@"WARNING: Authorized execution of helper tool returned failure status code %d", status); - NSError* err = [SCErr errorWithCode: 400]; - [SCSentry captureError: err]; + /// --60006 just means auth is cancelled, not really an "error" per se + if (status != -60006) { + NSError* err = [SCErr errorWithCode: 400]; + [SCSentry captureError: err]; - [NSApp presentError: err]; + [NSApp presentError: err]; + } return; } else { diff --git a/TimerWindowController.m b/TimerWindowController.m index a6599e07..05597183 100755 --- a/TimerWindowController.m +++ b/TimerWindowController.m @@ -326,8 +326,11 @@ - (IBAction)killBlock:(id)sender { &authorizationRef); if(status) { - NSLog(@"ERROR: Failed to authorize block kill."); - return; + if (status != -60006) { + NSError* err = [SCErr errorWithCode: 501]; + [SCSentry captureError: err]; + } + return; } // we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid From 42320452cfb6ff3864a15237afd00d1bc8b40f4a Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 17 Jan 2021 19:12:23 -0800 Subject: [PATCH 50/72] Tweaks to authorization, not quite there yet --- Common/SCXPCAuthorization.m | 48 ++++++++++++++++++------------------- Common/SCXPCClient.m | 23 ++++++++++-------- Daemon/SCDaemon.m | 8 ++++--- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/Common/SCXPCAuthorization.m b/Common/SCXPCAuthorization.m index 3f594f07..bea37f3c 100644 --- a/Common/SCXPCAuthorization.m +++ b/Common/SCXPCAuthorization.m @@ -86,37 +86,36 @@ + (NSDictionary *)commandInfo @"class": @"user", @"group": @"admin", @"timeout": @(300), // 5 minutes + @"shared": @(YES), @"version": @1 // not entirely sure what this does TBH }; } dispatch_once(&sOnceToken, ^{ #pragma clang diagnostic ignored "-Wundeclared-selector" + + + NSDictionary* startBlockCommandInfo = @{ + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.startBlock", + kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, + kCommandKeyAuthRightDesc : NSLocalizedString( + @"SelfControl needs your username and password to start the block.", + @"prompt shown when user is required to authorize to start block" + ) + }; + NSDictionary* modifyBlockCommandInfo = @{ + kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", + kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, + kCommandKeyAuthRightDesc : NSLocalizedString( + @"SelfControl needs your username and password to modify the block", + @"prompt shown when user is required to authorize to modify their block" + ) + }; + sCommandInfo = @{ - NSStringFromSelector(@selector(startBlockWithControllingUID:blocklist:isAllowlist:endDate:blockSettings:authorization:reply:)) : @{ - kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.startBlock", - kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, - kCommandKeyAuthRightDesc : NSLocalizedString( - @"SelfControl needs your username and password to start the block.", - @"prompt shown when user is required to authorize to start block" - ) - }, - NSStringFromSelector(@selector(updateBlocklist:authorization:reply:)) : @{ - kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", - kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, - kCommandKeyAuthRightDesc : NSLocalizedString( - @"SelfControl needs your username and password to modify the blocklist", - @"prompt shown when user is required to authorize to add to their blocklist" - ) - }, - NSStringFromSelector(@selector(updateBlockEndDate:authorization:reply:)) : @{ - kCommandKeyAuthRightName : @"org.eyebeam.SelfControl.modifyBlock", - kCommandKeyAuthRightDefault : kAuthorizationRuleAuthenticateAsAdmin5MinTimeout, - kCommandKeyAuthRightDesc : NSLocalizedString( - @"SelfControl needs your username and password to extend the block", - @"prompt shown when user is required to authorize to extend their blockc" - ) - } + NSStringFromSelector(@selector(startBlockWithControllingUID:blocklist:isAllowlist:endDate:blockSettings:authorization:reply:)) : startBlockCommandInfo, + NSStringFromSelector(@selector(updateBlocklist:authorization:reply:)) : modifyBlockCommandInfo, + NSStringFromSelector(@selector(updateBlockEndDate:authorization:reply:)) : modifyBlockCommandInfo #pragma clang diagnostic pop }; }); @@ -165,6 +164,7 @@ + (void)setupAuthorizationRights:(AuthorizationRef)authRef blockErr = AuthorizationRightGet([authRightName UTF8String], NULL); if (blockErr == errAuthorizationDenied) { + NSLog(@"setting auth right default for %@: %@", authRightName, authRightDefault); blockErr = AuthorizationRightSet( authRef, // authRef [authRightName UTF8String], // rightName diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index 84fdb1a6..c3aaeb5f 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -120,22 +120,25 @@ - (BOOL)isConnected { - (void)installDaemon:(void(^)(NSError*))callback { AuthorizationRef authorizationRef; - char* daemonPath = [self selfControlHelperToolPathUTF8String]; - NSUInteger daemonPathSize = strlen(daemonPath); - AuthorizationItem right = { - kSMRightBlessPrivilegedHelper, - daemonPathSize, - daemonPath, - 0 + AuthorizationItem blessRight = { + kSMRightBlessPrivilegedHelper, 0, NULL, 0 }; - AuthorizationRights authRights = { - 1, - &right + AuthorizationItem startBlockRight = { + "org.eyebeam.SelfControl.startBlock", 0, NULL, 0 }; + AuthorizationItem rightsArr[] = { blessRight, startBlockRight }; + + AuthorizationRights authRights; + authRights.count = 2; + authRights.items = rightsArr; + AuthorizationFlags myFlags = kAuthorizationFlagDefaults | kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed; OSStatus status; + +// AuthorizationItem sharedEnvItem = { kAuthorizationEnvironmentShared, 0, NULL, 0 }; +// AuthorizationEnvironment authEnv = { 1, &sharedEnvItem }; status = AuthorizationCreate (&authRights, kAuthorizationEmptyEnvironment, diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 6a401f59..4bc9db53 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -36,9 +36,11 @@ - (id) init { - (void)start { [self.listener resume]; - [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { - [SCDaemonBlockMethods checkupBlock]; - }]; + [NSTimer scheduledTimerWithTimeInterval: 5 repeats: NO block:^(NSTimer * _Nonnull timer) { + [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { + [SCDaemonBlockMethods checkupBlock]; + }]; + }]; } #pragma mark - NSXPCListenerDelegate From 7286489bcda3318c4efbf49145786ea7db960fd3 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Sun, 17 Jan 2021 21:16:25 -0800 Subject: [PATCH 51/72] Authorization caching works finally --- Common/SCXPCAuthorization.m | 59 +++++++++++++++------------------ Common/SCXPCClient.m | 65 +++++++++++++++++++++++-------------- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/Common/SCXPCAuthorization.m b/Common/SCXPCAuthorization.m index bea37f3c..76b1118b 100644 --- a/Common/SCXPCAuthorization.m +++ b/Common/SCXPCAuthorization.m @@ -23,9 +23,6 @@ + (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command // authData is expected to be an NSData with an AuthorizationExternalForm embedded inside. { #pragma unused(authData) - NSError * error; - OSStatus err; - OSStatus junk; AuthorizationRef authRef; assert(command != nil); @@ -33,45 +30,41 @@ + (NSError *)checkAuthorization:(NSData *)authData command:(SEL)command authRef = NULL; // First check that authData looks reasonable. - - error = nil; if ( (authData == nil) || ([authData length] != sizeof(AuthorizationExternalForm)) ) { - error = [NSError errorWithDomain:NSOSStatusErrorDomain code:paramErr userInfo:nil]; + return [NSError errorWithDomain:NSOSStatusErrorDomain code:paramErr userInfo:nil]; } // Create an authorization ref from that the external form data contained within. - - if (error == nil) { - err = AuthorizationCreateFromExternalForm([authData bytes], &authRef); - - // Authorize the right associated with the command. - - if (err == errAuthorizationSuccess) { - AuthorizationItem oneRight = { NULL, 0, NULL, 0 }; - AuthorizationRights rights = { 1, &oneRight }; - - oneRight.name = [[SCXPCAuthorization authorizationRightForCommand:command] UTF8String]; - assert(oneRight.name != NULL); - - err = AuthorizationCopyRights( - authRef, - &rights, - kAuthorizationEmptyEnvironment, - kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed, - NULL - ); - } - if (err != errAuthorizationSuccess) { - error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]; - } + OSStatus extFormStatus = AuthorizationCreateFromExternalForm([authData bytes], &authRef); + if (extFormStatus != errAuthorizationSuccess) { + return [NSError errorWithDomain: NSOSStatusErrorDomain code: extFormStatus userInfo: nil]; } + // Authorize the right associated with the command. + + AuthorizationItem oneRight = { NULL, 0, NULL, 0 }; + AuthorizationRights rights = { 1, &oneRight }; + AuthorizationFlags flags = kAuthorizationFlagDefaults | kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed; + + oneRight.name = [[SCXPCAuthorization authorizationRightForCommand:command] UTF8String]; + assert(oneRight.name != NULL); + + OSStatus authStatus = AuthorizationCopyRights( + authRef, + &rights, + kAuthorizationEmptyEnvironment, + flags, + NULL + ); if (authRef != NULL) { - junk = AuthorizationFree(authRef, 0); - assert(junk == errAuthorizationSuccess); + AuthorizationFree(authRef, 0); + } + + if (authStatus != errAuthorizationSuccess) { + return [NSError errorWithDomain: NSOSStatusErrorDomain code: authStatus userInfo: nil]; } - return error; + return nil; } diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index c3aaeb5f..7e2f5336 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -24,32 +24,50 @@ @interface SCXPCClient () { @implementation SCXPCClient - (void)setupAuthorization { - // this all mostly copied from Apple's Even Better Authorization Sample - OSStatus err; - AuthorizationExternalForm extForm; + // this is mostly copied from Apple's Even Better Authorization Sample // Create our connection to the authorization system. // // If we can't create an authorization reference then the app is not going to be able // to do anything requiring authorization. Generally this only happens when you launch - // the app in some wacky, and typically unsupported, way. In the debug build we flag that - // with an assert. In the release build we continue with self->_authRef as NULL, which will - // cause all authorized operations to fail. + // the app in some wacky, and typically unsupported, way. - err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, 0, &self->_authRef); - if (err == errAuthorizationSuccess) { - err = AuthorizationMakeExternalForm(self->_authRef, &extForm); - self.authorization = [[NSData alloc] initWithBytes: &extForm length: sizeof(extForm)]; + // if we've already got an authorization session, no need to make another + if (self.authorization) { + return; } - assert(err == errAuthorizationSuccess); - // If we successfully connected to Authorization Services, add definitions for our default - // rights (unless they're already in the database). + AuthorizationRef authRef; + OSStatus errCode = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, 0, &authRef); + if (errCode) { + NSError* err = [NSError errorWithDomain: NSOSStatusErrorDomain code: errCode userInfo: nil]; + NSLog(@"Failed to set up initial authorization with error %@", err); + [SCSentry captureError: err]; + } else { + [self updateStoredAuthorization: authRef]; + } +} + +- (void)updateStoredAuthorization:(AuthorizationRef)authRef { + self->_authRef = authRef; + if (!self->_authRef) { + self.authorization = nil; + return; + } - if (self->_authRef) { - [SCXPCAuthorization setupAuthorizationRights: self->_authRef]; + AuthorizationExternalForm extForm; + OSStatus errCode = AuthorizationMakeExternalForm(self->_authRef, &extForm); + if (errCode) { + NSError* err = [NSError errorWithDomain: NSOSStatusErrorDomain code: errCode userInfo: nil]; + NSLog(@"Failed to update stored authorization with error %@", err); + [SCSentry captureError: err]; + } else { + self.authorization = [[NSData alloc] initWithBytes: &extForm length: sizeof(extForm)]; } + // If we successfully connected to Authorization Services, add definitions for our default + // rights (unless they're already in the database). + [SCXPCAuthorization setupAuthorizationRights: self->_authRef]; } // Ensures that we're connected to our helper tool @@ -119,7 +137,6 @@ - (BOOL)isConnected { } - (void)installDaemon:(void(^)(NSError*))callback { - AuthorizationRef authorizationRef; AuthorizationItem blessRight = { kSMRightBlessPrivilegedHelper, 0, NULL, 0 }; @@ -137,13 +154,13 @@ - (void)installDaemon:(void(^)(NSError*))callback { kAuthorizationFlagInteractionAllowed; OSStatus status; -// AuthorizationItem sharedEnvItem = { kAuthorizationEnvironmentShared, 0, NULL, 0 }; -// AuthorizationEnvironment authEnv = { 1, &sharedEnvItem }; - - status = AuthorizationCreate (&authRights, - kAuthorizationEmptyEnvironment, - myFlags, - &authorizationRef); + status = AuthorizationCopyRights( + self->_authRef, + &authRights, + kAuthorizationEmptyEnvironment, + myFlags, + NULL + ); if(status) { // if it's just the user cancelling, make that obvious @@ -166,7 +183,7 @@ - (void)installDaemon:(void(^)(NSError*))callback { BOOL result = (BOOL)SMJobBless( kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), - authorizationRef, + self->_authRef, &cfError); if(!result) { From fb8f62bbe2eb27e7518b333afc71097e5b332ebe Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Mon, 18 Jan 2021 00:22:38 -0800 Subject: [PATCH 52/72] Switch daemon to unload on inactivity (vs unloading on block-end) --- AppController.m | 1 - Daemon/DaemonMain.m | 2 +- Daemon/SCDaemon.h | 16 ++++++ Daemon/SCDaemon.m | 99 +++++++++++++++++++++++++++++++++-- Daemon/SCDaemonBlockMethods.m | 28 ++++++---- cli-main.m | 1 - 6 files changed, 128 insertions(+), 19 deletions(-) diff --git a/AppController.m b/AppController.m index 4dbae646..0a65f3fc 100755 --- a/AppController.m +++ b/AppController.m @@ -617,7 +617,6 @@ - (void)installBlock { @"EnableErrorReporting": [self->defaults_ valueForKey: @"EnableErrorReporting"] } reply:^(NSError * _Nonnull error) { - NSLog(@"WOO started block with error %@", error); if (error != nil ) { if (![SCUtilities errorIsAuthCanceled: error]) { [NSApp performSelectorOnMainThread: @selector(presentError:) diff --git a/Daemon/DaemonMain.m b/Daemon/DaemonMain.m index 47e91a64..dcb44bb3 100644 --- a/Daemon/DaemonMain.m +++ b/Daemon/DaemonMain.m @@ -12,7 +12,7 @@ int main(int argc, const char *argv[]) { [SCSentry startSentry: @"org.eyebeam.selfcontrold"]; // get the daemon object going - SCDaemon* daemon = [[SCDaemon alloc] init]; + SCDaemon* daemon = [SCDaemon sharedDaemon]; [daemon start]; NSLog(@"running forever"); diff --git a/Daemon/SCDaemon.h b/Daemon/SCDaemon.h index fc015dcb..3f89e9c3 100644 --- a/Daemon/SCDaemon.h +++ b/Daemon/SCDaemon.h @@ -11,8 +11,24 @@ NS_ASSUME_NONNULL_BEGIN @interface SCDaemon : NSObject ++ (instancetype)sharedDaemon; + - (void)start; +// Starts checking up on the block on a regular basis +// to make sure it hasn't expired, been tampered with, etc +// (and will remove it or fix it if so) +- (void)startCheckupTimer; + +// Stops the checkup timer (this should only be called if there's +// no block running, because we should have checkups going for all blocks) +- (void)stopCheckupTimer; + +// Lets the daemon know that there was recent activity +// so we can reset our inactivity timer. +// The daemon will die if goes for too long without activity. +- (void)resetInactivityTimer; + @end NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 4bc9db53..1808f929 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -9,8 +9,12 @@ #import "SCDaemonProtocol.h" #import "SCDaemonXPC.h" #import"SCDaemonBlockMethods.h" +#import "SCUtilities.h" +#import "HostFileBlocker.h" +#import "SCDaemonUtilities.h" static NSString* serviceName = @"org.eyebeam.selfcontrold"; +float const INACTIVITY_LIMIT_SECS = 60 * 2; // 2 minutes @interface NSXPCConnection(PrivateAuditToken) @@ -22,11 +26,23 @@ @interface NSXPCConnection(PrivateAuditToken) @interface SCDaemon () @property (nonatomic, strong, readwrite) NSXPCListener* listener; +@property (strong, readwrite) NSTimer* checkupTimer; +@property (strong, readwrite) NSTimer* inactivityTimer; +@property (nonatomic, strong, readwrite) NSDate* lastActivityDate; @end @implementation SCDaemon ++ (instancetype)sharedDaemon { + static SCDaemon* daemon = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + daemon = [SCDaemon new]; + }); + return daemon; +} + - (id) init { _listener = [[NSXPCListener alloc] initWithMachServiceName: serviceName]; _listener.delegate = self; @@ -36,12 +52,85 @@ - (id) init { - (void)start { [self.listener resume]; - [NSTimer scheduledTimerWithTimeInterval: 5 repeats: NO block:^(NSTimer * _Nonnull timer) { - [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { - [SCDaemonBlockMethods checkupBlock]; - }]; + + // if there's any evidence of a block (i.e. an official one running, + // OR just block remnants remaining in hosts), we should start + // running checkup regularly so the block gets found/removed + // at the proper time. + // we do NOT run checkup if there's no block, because it can result + // in the daemon actually unloading itself before the app has a chance + // to start the block + if ([SCUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]) { + [self startCheckupTimer]; + } + + [self startInactivityTimer]; + [self resetInactivityTimer]; +} + +- (void)startCheckupTimer { + // this method must always be called on the main thread, so the timer will work properly + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self startCheckupTimer]; + }); + return; + } + + // if the timer's already running, don't stress it! + if (self.checkupTimer != nil) { + return; + } + + self.checkupTimer = [NSTimer scheduledTimerWithTimeInterval: 1 repeats: YES block:^(NSTimer * _Nonnull timer) { + [SCDaemonBlockMethods checkupBlock]; + }]; + + // run the first checkup immediately! + [SCDaemonBlockMethods checkupBlock]; +} +- (void)stopCheckupTimer { + if (self.checkupTimer == nil) { + return; + } + + [self.checkupTimer invalidate]; + self.checkupTimer = nil; +} + + +- (void)startInactivityTimer { + self.inactivityTimer = [NSTimer scheduledTimerWithTimeInterval: 15.0 repeats: YES block:^(NSTimer * _Nonnull timer) { + // we haven't had any activity in a while, the daemon appears to be idling + // so kill it to avoid the user having unnecessary processes running! + if ([[NSDate date] timeIntervalSinceDate: self.lastActivityDate] > INACTIVITY_LIMIT_SECS) { + // if we're inactive but also there's a block running, that's a bad thing + // start the checkups going again - unclear why they would've stopped + if ([SCUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]) { + [self startCheckupTimer]; + [SCDaemonBlockMethods checkupBlock]; + return; + } + + NSLog(@"Daemon inactive for more than %f seconds, exiting!", INACTIVITY_LIMIT_SECS); + [SCDaemonUtilities unloadDaemonJob]; + } }]; } +- (void)resetInactivityTimer { + self.lastActivityDate = [NSDate date]; +} + +- (void)dealloc { + if (self.checkupTimer) { + [self.checkupTimer invalidate]; + self.checkupTimer = nil; + } + if (self.inactivityTimer) { + [self.inactivityTimer invalidate]; + self.inactivityTimer = nil; + } +} #pragma mark - NSXPCListenerDelegate @@ -72,7 +161,7 @@ - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConne SCDaemonXPC* scdXPC = [[SCDaemonXPC alloc] init]; newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol: @protocol(SCDaemonProtocol)]; newConnection.exportedObject = scdXPC; - + [newConnection resume]; NSLog(@"Accepted new connection!"); diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 58f22427..42946b9c 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -11,6 +11,7 @@ #import "PacketFilter.h" #import "SCDaemonUtilities.h" #import "BlockManager.h" +#import "SCDaemon.h" NSTimeInterval METHOD_LOCK_TIMEOUT = 5.0; NSTimeInterval CHECKUP_LOCK_TIMEOUT = 0.5; // use a shorter lock timeout for checkups, because we'd prefer not to have tons pile up @@ -51,6 +52,11 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData NSLog(@"INFO: Blocklist successfully updated."); reply(nil); + [[SCDaemon sharedDaemon] resetInactivityTimer]; [self.daemonMethodLock unlock]; } @@ -241,6 +250,8 @@ + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData [SCSentry addBreadcrumb: @"Daemon extended block successfully" category: @"daemon"]; NSLog(@"INFO: Block successfully extended."); reply(nil); + + [[SCDaemon sharedDaemon] resetInactivityTimer]; [self.daemonMethodLock unlock]; } @@ -280,13 +291,9 @@ + (void)checkupBlock { // [settings synchronizeSettings]; // - [SCDaemonUtilities unloadDaemonJob]; - - // execution should never reach this point because we've unloaded - exit(EX_SOFTWARE); - } - - if (![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { + // once the checkups stop, the daemon will clear itself in a while due to inactivity + [[SCDaemon sharedDaemon] stopCheckupTimer]; + } else if (![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]) { NSLog(@"INFO: Checkup ran, block expired, removing block."); removeBlock(); @@ -295,10 +302,8 @@ + (void)checkupBlock { [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; - [SCDaemonUtilities unloadDaemonJob]; - - // execution should never reach this point because we've unloaded - exit(EX_SOFTWARE); + // once the checkups stop, the daemon will clear itself in a while due to inactivity + [[SCDaemon sharedDaemon] stopCheckupTimer]; } else if ([[NSDate date] timeIntervalSinceDate: lastBlockIntegrityCheck] > integrityCheckIntervalSecs) { lastBlockIntegrityCheck = [NSDate date]; // The block is still on. Every once in a while, we should @@ -340,6 +345,7 @@ + (void)checkupBlock { } else NSLog(@"INFO: Checkup ran with integrity check, no action needed."); } + [[SCDaemon sharedDaemon] resetInactivityTimer]; [self.daemonMethodLock unlock]; } diff --git a/cli-main.m b/cli-main.m index 457783bf..80e68258 100755 --- a/cli-main.m +++ b/cli-main.m @@ -148,7 +148,6 @@ int main(int argc, char* argv[]) { endDate: blockEndDate blockSettings: blockSettings reply:^(NSError * _Nonnull error) { - NSLog(@"WOO started block with error %@", error); if (error != nil) { NSLog(@"ERROR: Daemon failed to start block with error %@", error); exit(EX_SOFTWARE); From db8fefaba8bad410cc7b3bbdc330adf10d9383a8 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 00:20:58 -0800 Subject: [PATCH 53/72] Get SCUtilities unit tests passing again --- AppController.m | 3 +- Common/SCSentry.h | 3 +- Common/SCSentry.m | 20 +++++++++++ Common/SCSettings.h | 2 +- Common/SCSettings.m | 18 ++++++---- Common/SCUtilities.h | 3 +- Common/SCUtilities.m | 25 +++++-------- Daemon/SCDaemonBlockMethods.m | 4 +-- SelfControl.xcodeproj/project.pbxproj | 3 ++ SelfControlTests/SCUtilitiesTests.m | 52 ++++++++++++--------------- 10 files changed, 73 insertions(+), 60 deletions(-) diff --git a/AppController.m b/AppController.m index 0a65f3fc..5a4f4621 100755 --- a/AppController.m +++ b/AppController.m @@ -31,7 +31,6 @@ #import "SCSettings.h" #import #import "SCXPCClient.h" -#import #import "HostFileBlocker.h" @interface AppController () {} @@ -697,7 +696,7 @@ - (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToA [self.xpc refreshConnectionAndRun:^{ // Before we try to extend the block, make sure the block time didn't run out (or is about to run out) in the meantime - if (![SCUtilities blockShouldBeRunningInDictionary: self->settings_.dictionaryRepresentation] || [oldBlockEndDate timeIntervalSinceNow] < 1) { + if ([SCUtilities currentBlockIsExpired] || [oldBlockEndDate timeIntervalSinceNow] < 1) { // we're done, or will be by the time we get to it! so just let it expire. they can restart it. [lockToUse unlock]; return; diff --git a/Common/SCSentry.h b/Common/SCSentry.h index 63c1764e..f48b3caa 100644 --- a/Common/SCSentry.h +++ b/Common/SCSentry.h @@ -6,7 +6,8 @@ // #import -#import + +@class SentryScope; NS_ASSUME_NONNULL_BEGIN diff --git a/Common/SCSentry.m b/Common/SCSentry.m index 0a533203..4347f2ed 100644 --- a/Common/SCSentry.m +++ b/Common/SCSentry.m @@ -8,10 +8,15 @@ #import "SCSentry.h" #import "SCSettings.h" +#ifndef TESTING +#import +#endif + @implementation SCSentry //org.eyebeam.SelfControl + (void)startSentry:(NSString*)componentId { +#ifndef TESTING [SentrySDK startWithConfigureOptions:^(SentryOptions *options) { options.dsn = @"https://58fbe7145368418998067f88896007b2@o504820.ingest.sentry.io/5592195"; options.debug = YES; // Enabled debug when first installing is always helpful @@ -31,9 +36,16 @@ + (void)startSentry:(NSString*)componentId { [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { [scope setTagValue: [[NSLocale currentLocale] localeIdentifier] forKey: @"localeId"]; }]; +#endif } + (BOOL)errorReportingEnabled { +#ifdef TESTING + // don't report to Sentry while unit-testing! + if ([[NSUserDefaults standardUserDefaults] boolForKey: @"isTest"]) { + return YES; + } +#endif if (geteuid() != 0) { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; return [defaults boolForKey: @"EnableErrorReporting"]; @@ -91,17 +103,21 @@ + (void)updateDefaultsContext { [defaultsDict removeObjectForKey: @"Blocklist"]; [defaultsDict removeObjectForKey: @"SULastCheckTime"]; +#ifndef TESTING [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { [scope setContextValue: defaultsDict forKey: @"NSUserDefaults"]; }]; +#endif } + (void)addBreadcrumb:(NSString*)message category:(NSString*)category { +#ifndef TESTING SentryBreadcrumb* crumb = [[SentryBreadcrumb alloc] init]; crumb.level = kSentryLevelInfo; crumb.category = category; crumb.message = message; [SentrySDK addBreadcrumb: crumb]; +#endif } + (void)captureError:(NSError*)error { @@ -121,7 +137,9 @@ + (void)captureError:(NSError*)error { NSLog(@"Reporting error %@ to Sentry...", error); [[SCSettings sharedSettings] updateSentryContext]; [SCSentry updateDefaultsContext]; +#ifndef TESTING [SentrySDK captureError: error]; +#endif } + (void)captureMessage:(NSString*)message withScopeBlock:(nullable void (^)(SentryScope * _Nonnull))block { @@ -142,11 +160,13 @@ + (void)captureMessage:(NSString*)message withScopeBlock:(nullable void (^)(Sent [[SCSettings sharedSettings] updateSentryContext]; [SCSentry updateDefaultsContext]; +#ifndef TESTING if (block != nil) { [SentrySDK captureMessage: message withScopeBlock: block]; } else { [SentrySDK captureMessage: message]; } +#endif } + (void)captureMessage:(NSString*)message { diff --git a/Common/SCSettings.h b/Common/SCSettings.h index f60f9014..37d9857a 100644 --- a/Common/SCSettings.h +++ b/Common/SCSettings.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly) uid_t userId; @property (readonly) NSDictionary* dictionaryRepresentation; -@property (nonatomic, readonly, getter=isReadOnly) BOOL readOnly; +@property (nonatomic, getter=isReadOnly) BOOL readOnly; @property (class, nonatomic, readonly) NSString* settingsFileName; @property (class, nonatomic, readonly) NSString* securedSettingsFilePath; diff --git a/Common/SCSettings.m b/Common/SCSettings.m index e57fd6e6..f025105f 100644 --- a/Common/SCSettings.m +++ b/Common/SCSettings.m @@ -11,6 +11,10 @@ #import "SCUtilities.h" #import +#ifndef TESTING +#import +#endif + float const SYNC_INTERVAL_SECS = 30; float const SYNC_LEEWAY_SECS = 30; NSString* const SETTINGS_FILE_DIR = @"/usr/local/etc/"; @@ -249,12 +253,12 @@ - (void)writeSettingsWithCompletion:(nullable void(^)(NSError* _Nullable))comple return; } - if ([[NSUserDefaults standardUserDefaults] boolForKey: @"isTest"]) { - // no writing to disk during unit tests - NSLog(@"Would write settings to disk now (but no writing during unit tests)"); - if (completionBlock != nil) completionBlock(nil); - return; - } +#if TESTING + // no writing to disk during unit tests + NSLog(@"Would write settings to disk now (but no writing during unit tests)"); + if (completionBlock != nil) completionBlock(nil); + return; +#endif // don't spend time on the main thread writing out files - it's OK for this to happen without blocking other things dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -502,9 +506,11 @@ - (void)updateSentryContext { timeStyle: NSDateFormatterFullStyle]; } +#ifndef TESTING [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { [scope setContextValue: dictCopy forKey: @"SCSettings"]; }]; +#endif } - (void)onSettingChanged:(NSNotification*)note { diff --git a/Common/SCUtilities.h b/Common/SCUtilities.h index fe70d8e2..c25fc902 100644 --- a/Common/SCUtilities.h +++ b/Common/SCUtilities.h @@ -39,8 +39,7 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu + (BOOL)modernBlockIsRunning; + (BOOL)legacyBlockIsRunning; -+ (BOOL) blockIsRunningInDictionary:(NSDictionary*)dict; -+ (BOOL) blockShouldBeRunningInDictionary:(NSDictionary *)dict; ++ (BOOL) currentBlockIsExpired; + (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL; + (BOOL) blockIsRunningInLegacyDictionary:(NSDictionary*)dict; diff --git a/Common/SCUtilities.m b/Common/SCUtilities.m index b6275dc5..9ee6b451 100644 --- a/Common/SCUtilities.m +++ b/Common/SCUtilities.m @@ -156,11 +156,7 @@ + (BOOL)anyBlockIsRunning { + (BOOL)modernBlockIsRunning { SCSettings* settings = [SCSettings sharedSettings]; - if ([SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]) { - return YES; - } - - return NO; + return [settings boolForKey: @"BlockIsRunning"]; } + (BOOL)legacyBlockIsRunning { @@ -199,26 +195,21 @@ + (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL { return [SCUtilities blockIsRunningInLegacyDictionary: legacySettingsDict]; } -// returns YES if a block is actively running (to the best of our knowledge), and NO otherwise -+ (BOOL) blockIsRunningInDictionary:(NSDictionary *)dict { - // simple: the block is running if BlockIsRunning is set to true! - return [[dict valueForKey: @"BlockIsRunning"] boolValue]; -} - -// returns YES if the block should be active based on the specified end time (i.e. it is in the future), or NO otherwise -+ (BOOL) blockShouldBeRunningInDictionary:(NSDictionary *)dict { +// returns YES if the block should have expired active based on the specified end time (i.e. the end time is in the past), or NO otherwise ++ (BOOL)currentBlockIsExpired { // the block should be running if the end date hasn't arrived yet - if ([[dict objectForKey: @"BlockEndDate"] timeIntervalSinceNow] > 0) { - return YES; - } else { + SCSettings* settings = [SCSettings sharedSettings]; + if ([[settings valueForKey: @"BlockEndDate"] timeIntervalSinceNow] > 0) { return NO; + } else { + return YES; } } + (void) removeBlockFromSettings { SCSettings* settings = [SCSettings sharedSettings]; + [settings setValue: @NO forKey: @"BlockIsRunning"]; [settings setValue: nil forKey: @"BlockEndDate"]; - [settings setValue: nil forKey: @"BlockIsRunning"]; [settings setValue: nil forKey: @"ActiveBlocklist"]; [settings setValue: nil forKey: @"ActiveBlockAsWhitelist"]; } diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 42946b9c..678294eb 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -95,7 +95,7 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray #import "SCUtilities.h" +#import "SCSentry.h" +#import "SCErr.h" #import "SCSettings.h" @interface SCUtilitiesTests : XCTestCase @@ -31,6 +33,10 @@ - (NSUserDefaults*)testDefaults { } + (void)setUp { + // SCSettings shouldn't be readOnly during our tests + // so we can test changing values + [SCSettings sharedSettings].readOnly = NO; + // Initialize the sample legacy setting dictionaries activeBlockLegacyDict = @{ @"BlockStartedDate": [NSDate dateWithTimeIntervalSinceNow: -300], // 5 minutes ago @@ -132,42 +138,30 @@ - (void) testCleanBlocklistEntries { XCTAssert([cleaned[4] isEqualToString: @"reader.google.com"]); } -- (void) testStartingAndRemovingBlocks { - SCSettings* settings = [SCSettings sharedSettings]; - - XCTAssert(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]); - XCTAssert(![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); - - // test starting a block - [SCUtilities startBlockInSettings: settings withBlockDuration: 21600]; - XCTAssert([SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); - NSTimeInterval timeToBlockEnd = [[settings valueForKey: @"BlockEndDate"] timeIntervalSinceNow]; - XCTAssert(round(timeToBlockEnd) == 21600); - - // test removing a block - [SCUtilities removeBlockFromSettings]; - XCTAssert(![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); -} - (void) testModernBlockDetection { SCSettings* settings = [SCSettings sharedSettings]; - XCTAssert(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]); - XCTAssert(![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); - - // test starting a block - [SCUtilities startBlockInSettings: settings withBlockDuration: 21600]; - XCTAssert(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]); - XCTAssert([SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); + XCTAssert(![SCUtilities modernBlockIsRunning]); + XCTAssert([SCUtilities currentBlockIsExpired]); - // turn the block "on" + // test a block that should have expired 5 minutes ago [settings setValue: @YES forKey: @"BlockIsRunning"]; - XCTAssert([SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]); - XCTAssert([SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); + [settings setValue: @[ @"facebook.com", @"reddit.com" ] forKey: @"ActiveBlocklist"]; + [settings setValue: @NO forKey: @"ActiveBlockAsWhitelist"]; + [settings setValue: [NSDate dateWithTimeIntervalSinceNow: -300] forKey: @"BlockEndDate"]; - // remove the block + XCTAssert([SCUtilities modernBlockIsRunning]); + XCTAssert([SCUtilities currentBlockIsExpired]); + + // test block that should still be running + [settings setValue: [NSDate dateWithTimeIntervalSinceNow: 300] forKey: @"BlockEndDate"]; + XCTAssert([SCUtilities modernBlockIsRunning]); + XCTAssert(![SCUtilities currentBlockIsExpired]); + + // test removing a block [SCUtilities removeBlockFromSettings]; - XCTAssert(![SCUtilities blockIsRunningInDictionary: settings.dictionaryRepresentation]); - XCTAssert(![SCUtilities blockShouldBeRunningInDictionary: settings.dictionaryRepresentation]); + XCTAssert(![SCUtilities modernBlockIsRunning]); + XCTAssert([SCUtilities currentBlockIsExpired]); } - (void) testLegacyBlockDetection { From 7aef629f0796a2a819645c41ceea30706fd0a4f7 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 16:48:16 -0800 Subject: [PATCH 54/72] Speed up Run Script to speed up builds --- SelfControl.xcodeproj/project.pbxproj | 22 ++++++++++++++++++++-- selfcontrol-cli-Info.plist | 6 +++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 33fbcf09..4b705eb3 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -773,6 +773,7 @@ isa = PBXNativeTarget; buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "SelfControl" */; buildPhases = ( + CB81A92E25B7B4B8006956F7 /* ShellScript */, 10CA088CE7CF641F4F172445 /* [CP] Check Pods Manifest.lock */, 8D1107290486CEB800E47090 /* Resources */, 8D11072C0486CEB800E47090 /* Sources */, @@ -1156,7 +1157,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "buildNumber=$(xcodebuild -project SelfControl.xcodeproj/ -showBuildSettings | grep \"CURRENT_PROJECT_VERSION\" | sed 's/[ ]*CURRENT_PROJECT_VERSION = //')\n\necho \"#define SELFCONTROL_VERSION_STRING @\\\"${buildNumber}\\\"\" > \"${PROJECT_DIR}/version-header.h\"\n"; + shellScript = "# buildNumber=$(xcodebuild -project SelfControl.xcodeproj/ -showBuildSettings | grep \"CURRENT_PROJECT_VERSION\" | sed 's/[ ]*CURRENT_PROJECT_VERSION = //')\necho \"#define SELFCONTROL_VERSION_STRING @\\\"${MARKETING_VERSION}\\\"\" > \"${PROJECT_DIR}/version-header.h\"\n"; }; CB5E5FF81C3A5FD10038F331 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -1186,7 +1187,24 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "buildNumber=$(xcodebuild -project SelfControl.xcodeproj/ -showBuildSettings | grep \"CURRENT_PROJECT_VERSION\" | sed 's/[ ]*CURRENT_PROJECT_VERSION = //')\n\necho \"#define SELFCONTROL_VERSION_STRING @\\\"${buildNumber}\\\"\" > \"${PROJECT_DIR}/version-header.h\"\n"; + shellScript = "echo \"#define SELFCONTROL_VERSION_STRING @\\\"${MARKETING_VERSION}\\\"\" > \"${PROJECT_DIR}/version-header.h\"\n"; + }; + CB81A92E25B7B4B8006956F7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\necho \"#define SELFCONTROL_VERSION_STRING @\\\"${MARKETING_VERSION}\\\"\" > \"${PROJECT_DIR}/version-header.h\"\n"; }; FFF74CE7FF453369A49FFE5B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/selfcontrol-cli-Info.plist b/selfcontrol-cli-Info.plist index 2a7cf08d..6cdc6da2 100644 --- a/selfcontrol-cli-Info.plist +++ b/selfcontrol-cli-Info.plist @@ -43,17 +43,17 @@ $(CURRENT_PROJECT_VERSION) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - Free and open-source under the GPL. NSAppTransportSecurity NSAllowsArbitraryLoads + NSHumanReadableCopyright + Free and open-source under the GPL. SMPrivilegedExecutables org.eyebeam.selfcontrold - anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) + anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) From 04297e5b220f520df7748f987eef3bbcb5941b93 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 17:07:33 -0800 Subject: [PATCH 55/72] Split off SCMigrationUtilities from main utilities class --- AppController.h | 1 - AppController.m | 4 +- Common/HelperCommon.h | 1 - Common/SCMigrationUtilities.h | 34 ++++ Common/SCMigrationUtilities.m | 277 ++++++++++++++++++++++++++ Common/SCSettings.h | 1 - Common/SCUtilities.h | 11 +- Common/SCUtilities.m | 260 +----------------------- Daemon/SCDaemonBlockMethods.m | 6 +- DomainListWindowController.h | 1 - SCKillerHelper/main.m | 9 +- SelfControl.xcodeproj/project.pbxproj | 20 +- SelfControlCommon.h | 27 --- SelfControlTests/SCUtilitiesTests.m | 16 +- TimerWindowController.h | 1 - TimerWindowController.m | 4 +- 16 files changed, 352 insertions(+), 321 deletions(-) create mode 100644 Common/SCMigrationUtilities.h create mode 100644 Common/SCMigrationUtilities.m delete mode 100644 SelfControlCommon.h diff --git a/AppController.h b/AppController.h index c1778dbe..7936d9b0 100755 --- a/AppController.h +++ b/AppController.h @@ -29,7 +29,6 @@ #import #import #import -#import "SelfControlCommon.h" #import "SCSettings.h" // The main controller for the SelfControl app, which includes several methods diff --git a/AppController.m b/AppController.m index 5a4f4621..f3d5a140 100755 --- a/AppController.m +++ b/AppController.m @@ -304,8 +304,8 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { settings_ = [SCSettings sharedSettings]; // go copy over any preferences from legacy setting locations // (we won't clear any old data yet - we leave that to the daemon) - if ([SCUtilities legacySettingsFoundForCurrentUser]) { - [SCUtilities copyLegacySettingsToDefaults]; + if ([SCMigrationUtilities legacySettingsFoundForCurrentUser]) { + [SCMigrationUtilities copyLegacySettingsToDefaults]; } // start up our daemon XPC diff --git a/Common/HelperCommon.h b/Common/HelperCommon.h index f1aa4664..6ab8d9eb 100644 --- a/Common/HelperCommon.h +++ b/Common/HelperCommon.h @@ -28,7 +28,6 @@ #import #import #import "HostFileBlocker.h" -#import "SelfControlCommon.h" #import "SCUtilities.h" #import "SCSettings.h" diff --git a/Common/SCMigrationUtilities.h b/Common/SCMigrationUtilities.h new file mode 100644 index 00000000..44ee95d6 --- /dev/null +++ b/Common/SCMigrationUtilities.h @@ -0,0 +1,34 @@ +//SCMigrationUtilities// SCMigration.h +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// Utility methods dealing with legacy settings, legacy blocks, +// and migrating us from old versions of the app to the new one + +@interface SCMigrationUtilities : NSObject + ++ (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId; + ++ (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID; ++ (BOOL)legacySettingsFoundForCurrentUser; ++ (BOOL)legacyLockFileExists; + ++ (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL; ++ (BOOL)blockIsRunningInLegacyDictionary:(NSDictionary*)dict; + ++ (NSDate*)legacyBlockEndDate; + ++ (void)copyLegacySettingsToDefaults:(uid_t)controllingUID; ++ (void)copyLegacySettingsToDefaults; + ++ (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Common/SCMigrationUtilities.m b/Common/SCMigrationUtilities.m new file mode 100644 index 00000000..65003d87 --- /dev/null +++ b/Common/SCMigrationUtilities.m @@ -0,0 +1,277 @@ +// +// SCMigrationUtilities.m +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#define SelfControlLegacyLockFilePath @"/etc/SelfControl.lock" + +#import "SCMigrationUtilities.h" +#import +#import "SCSettings.h" +#import "SCUtilities.h" + +@implementation SCMigrationUtilities + ++ (NSString*)homeDirectoryForUid:(uid_t)uid { + struct passwd *pwd = getpwuid(uid); + return [NSString stringWithCString: pwd->pw_dir encoding: NSString.defaultCStringEncoding]; +} + ++ (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId { + NSString* homeDir = [SCMigrationUtilities homeDirectoryForUid: userId]; + return [[NSString stringWithFormat: @"%@/Library/Preferences/%@", homeDir, SCSettings.settingsFileName] stringByExpandingTildeInPath]; +} + +// check all legacy settings (old secured settings, lockfile, old-school defaults) +// to see if there's anything there ++ (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID { + NSFileManager* fileMan = [NSFileManager defaultManager]; + NSString* legacySettingsPath = [SCMigrationUtilities legacySecuredSettingsFilePathForUser: controllingUID]; + NSArray* defaultsHostBlacklist; + + if (geteuid() == 0 && controllingUID) { + // we're running as root, so get the defaults dictionary using our special function) + NSDictionary* defaultsDict = [SCUtilities defaultsDictForUser: controllingUID]; + defaultsHostBlacklist = defaultsDict[@"HostBlacklist"]; + } else { + // normal times, just use standard defaults + defaultsHostBlacklist = [[NSUserDefaults standardUserDefaults] objectForKey: @"HostBlacklist"]; + } + + return defaultsHostBlacklist || [fileMan fileExistsAtPath: legacySettingsPath] || [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]; +} ++ (BOOL)legacySettingsFoundForCurrentUser { + return [SCMigrationUtilities legacySettingsFoundForUser: getuid()]; +} + ++ (BOOL)legacyLockFileExists { + return [[NSFileManager defaultManager] fileExistsAtPath: SelfControlLegacyLockFilePath]; +} + ++ (NSDate*)legacyBlockEndDate { + // if we're running this as a normal user (generally that means app/CLI), it's easy: just get the standard user defaults + // this method can't be run as root, it won't work + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary* lockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLegacyLockFilePath]; + NSString* legacySettingsPath = [SCMigrationUtilities legacySecuredSettingsFilePathForUser: getuid()]; + NSDictionary* settingsFromDisk = [NSDictionary dictionaryWithContentsOfFile: legacySettingsPath]; + + // if we have a v3.x settings dictionary, take from that + if (settingsFromDisk != nil && settingsFromDisk[@"BlockEndDate"] != nil) { + return settingsFromDisk[@"BlockEndDate"]; + } + + // otherwise, we can look in defaults or the lockfile, both from pre-3.x versions + // these would have BlockStartedDate + BlockDuration instead of BlockEndDate, so conversion is needed + NSDate* startDate = [defaults objectForKey: @"BlockStartedDate"]; + NSTimeInterval duration = [defaults floatForKey: @"BlockDuration"]; + + // if defaults didn't have valid values, try the lockfile + if (startDate == nil || [startDate timeIntervalSinceNow] >= 0 || duration <= 0) { + startDate = lockDict[@"BlockStartedDate"]; + duration = [lockDict[@"BlockStartedDate"] floatValue]; + } + if (startDate == nil || [startDate timeIntervalSinceNow] >= 0 || duration <= 0) { + // if still not, we give up! no end date found, so call it the past + return [NSDate distantPast]; + } + return [startDate dateByAddingTimeInterval: (duration * 60)]; +} + ++ (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL { + NSDictionary* legacySettingsDict = [NSDictionary dictionaryWithContentsOfURL: settingsFileURL]; + + // if the file doesn't exist, there's definitely no block + if (legacySettingsDict == nil) return NO; + + return [SCMigrationUtilities blockIsRunningInLegacyDictionary: legacySettingsDict]; +} + ++ (BOOL)blockIsRunningInLegacyDictionary:(NSDictionary*)dict { + if (dict == nil) return NO; + + NSDate* blockStartedDate = [dict objectForKey:@"BlockStartedDate"]; + BOOL blockIsRunningValue = [[dict objectForKey: @"BlockIsRunning"] boolValue]; + + // for v3.0-3.0.3: the block is running if the BlockIsRunning key is true + // super old legacy (pre-3.0): the block is running if BlockStartedDate exists and isn't equal to the default value + if (blockIsRunningValue || (blockStartedDate != nil && ![blockStartedDate isEqualToDate: [NSDate distantFuture]])) { + return YES; + } else { + return NO; + } +} + +// copies settings from legacy locations (user-based secured settings used from 3.0-3.0.3, +// or older defaults/lockfile used pre-3.0) to their modern destinations in NSUserDefaults. +// does NOT update any of the values in SCSettings, and does NOT clear out settings from anywhere +// that makes this safe to call anytime, includig while a block is running ++ (void)copyLegacySettingsToDefaults:(uid_t)controllingUID { + NSLog(@"Copying legacy settings to defaults..."); + BOOL runningAsRoot = (geteuid() == 0); + if (runningAsRoot && !controllingUID) { + // if we're running as root, but we didn't get a valid non-root controlling UID + // we don't really have anywhere to copy those legacy settings to, because root doesn't have defaults + NSLog(@"WARNING: Can't copy legacy settings to defaults, because SCSettings is being run as root and no controlling UID was sent."); + return; + } + if (!controllingUID) controllingUID = getuid(); + + NSDictionary* defaultDefaults = SCConstants.defaultUserDefaults; + // if we're running this as a normal user (generally that means app/CLI), it's easy: just get the standard user defaults + // if we're running this as root, we need to be given a UID target, then we imitate them to grab their defaults + NSUserDefaults* defaults; + if (runningAsRoot) { + seteuid(controllingUID); + defaults = [NSUserDefaults standardUserDefaults]; + [defaults addSuiteNamed: @"org.eyebeam.SelfControl"]; + [defaults synchronize]; + } else { + defaults = [NSUserDefaults standardUserDefaults]; + } + + NSDictionary* lockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLegacyLockFilePath]; + + NSString* legacySettingsPath = [SCMigrationUtilities legacySecuredSettingsFilePathForUser: controllingUID]; + NSDictionary* settingsFromDisk = [NSDictionary dictionaryWithContentsOfFile: legacySettingsPath]; + + // if we have a v3.x settings dictionary, copy what we can from that + if (settingsFromDisk != nil) { + NSLog(@"Migrating all settings from legacy secured settings file %@", legacySettingsPath); + + // we assume the settings from disk are newer / should override existing values + // UNLESS the user has set a default to its non-default value + + // we'll look at all the possible keys in defaults - some of them should really + // have never ended up in settings at any point, but shouldn't matter + for (NSString* key in [defaultDefaults allKeys]) { + id settingsValue = settingsFromDisk[key]; + id defaultsValue = [defaults objectForKey: key]; + + // we have a value from settings, and the defaults value is unset or equal to the default value + // so pull the value from settings in! + if (settingsValue != nil && (defaultsValue == nil || [defaultsValue isEqualTo: defaultDefaults[key]])) { + NSLog(@"Migrating keypair (%@, %@) from settings to defaults", key, settingsValue); + [defaults setObject: settingsValue forKey: key]; + } + } + + NSLog(@"Done migrating preferences from legacy secured settings to defaults!"); + } + + // if we're on a pre-3.0 version, we may need to migrate the blocklist from defaults or the lock dictionary + // the Blocklist attribute used to be named HostBlacklist, so needs a special migration + NSArray* blocklistInDefaults = [defaults arrayForKey: @"Blocklist"]; + // of course, don't overwrite if we already have a blocklist in today's defaults + if (blocklistInDefaults == nil || blocklistInDefaults.count == 0) { + if (lockDict != nil && lockDict[@"HostBlacklist"] != nil) { + [defaults setObject: lockDict[@"HostBlacklist"] forKey: @"Blocklist"]; + NSLog(@"Migrated blocklist from pre-3.0 lock dictionary: %@", lockDict[@"HostBlacklist"]); + } else if ([defaults objectForKey: @"HostBlacklist"] != nil) { + [defaults setObject: [defaults objectForKey: @"HostBlacklist"] forKey: @"Blocklist"]; + NSLog(@"Migrated blocklist from pre-3.0 legacy defaults: %@", [defaults objectForKey: @"HostBlacklist"]); + } + } + + // if we're running as root and imitated the user to get their defaults, we need to put things back in place when done + if (runningAsRoot) { + [NSUserDefaults resetStandardUserDefaults]; + seteuid(0); + } + + [SCSentry addBreadcrumb: @"Copied legacy settings to defaults successfully" category: @"settings"]; + NSLog(@"Done copying settings!"); +} + ++ (void)copyLegacySettingsToDefaults { + [SCMigrationUtilities copyLegacySettingsToDefaults: 0]; +} + +// We might have "legacy" block settings hiding in one of three places: +// - a "lock file" at /etc/SelfControl.lock (aka SelfControlLegacyLockFilePath) +// - the defaults system +// - a v3.x per-user secured settings file +// we should check for block settings in all of these places and get rid of them ++ (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID { + NSLog(@"Clearing legacy settings!"); + + BOOL runningAsRoot = (geteuid() == 0); + if (!runningAsRoot || !controllingUID) { + // if we're not running as root, or we didn't get a valid non-root controlling UID + // we won't have permissions to make this work. This method MUST be called with root perms + NSLog(@"ERROR: Can't clear legacy settings, because we aren't running as root."); + NSError* err = [SCErr errorWithCode: 701]; + [SCSentry captureError: err]; + return err; + } + + // if we're gonna clear settings, there can't be a block running anywhere. otherwise, we should wait! + if ([SCUtilities legacyBlockIsRunning]) { + NSLog(@"ERROR: Can't clear legacy settings because a block is ongoing!"); + NSError* err = [SCErr errorWithCode: 702]; + [SCSentry captureError: err]; + return err; + } + + NSFileManager* fileMan = [NSFileManager defaultManager]; + + // besides Blocklist and the values copied from the v3.0-3.0.3 settings file to defaults in copyLegacySettingsToDefaults + // we actually don't need to move anything else over! Why? + // 1. The other settings from 3.0-3.0.3 don't matter as long as a block isn't running (i.e. BlockIsRunning should be false + // and BlockEndDate shouldn't be set). + // 2. All of the non-block settings from pre-3.0 can stay in defaults, ahd BlockStartedDate should be false if no block running + // so all that's left is to clear out the legacy crap for good + + // if an error happens trying to clear any portion of the old settings, + // we'll remember it, log it, and return it, but still try to clear the rest (best-effort) + NSError* retErr = nil; + + // first, clear the pre-3.0 lock dictionary + if(![fileMan removeItemAtPath: SelfControlLegacyLockFilePath error: &retErr] && [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]) { + NSLog(@"WARNING: Could not remove legacy SelfControl lock file because of error: %@", retErr); + [SCSentry captureError: retErr]; + } + + // then, clear keys out of defaults which aren't used + // prepare defaults by imitating the appropriate user + seteuid(controllingUID); + NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; + [defaults addSuiteNamed: @"org.eyebeam.SelfControl"]; + [defaults synchronize]; + NSArray* defaultsKeysToClear = @[ + @"BlockStartedDate", + @"BlockEndDate", + @"HostBlacklist" + ]; + for (NSString* key in defaultsKeysToClear) { + [defaults removeObjectForKey: key]; + } + [defaults synchronize]; + [NSUserDefaults resetStandardUserDefaults]; + seteuid(0); + + // clear all legacy per-user secured settings (v3.0-3.0.3) in every user's home folder + NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &retErr]; + if (homeDirectoryURLs != nil) { + for (NSURL* homeDirURL in homeDirectoryURLs) { + NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; + NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; + + if(![fileMan removeItemAtURL: settingsFileURL error: &retErr] && [fileMan fileExistsAtPath: settingsFileURL.path]) { + NSLog(@"WARNING: Could not remove legacy SelfControl settings file at URL %@ because of error: %@", settingsFileURL, retErr); + [SCSentry captureError: retErr]; + } + } + } + + // and that's it! note that we don't touch the modern SCSettings at all, and that's OK - it'll restart from scratch and be fine + [SCSentry addBreadcrumb: @"Cleared legacy settings successfully" category: @"settings"]; + NSLog(@"Cleared legacy settings!"); + + return retErr; +} + + +@end diff --git a/Common/SCSettings.h b/Common/SCSettings.h index 37d9857a..10ebcf7b 100644 --- a/Common/SCSettings.h +++ b/Common/SCSettings.h @@ -6,7 +6,6 @@ // #import -#import "SelfControlCommon.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Common/SCUtilities.h b/Common/SCUtilities.h index c25fc902..e7585ff4 100644 --- a/Common/SCUtilities.h +++ b/Common/SCUtilities.h @@ -6,6 +6,7 @@ // #import +#import "SCMigrationUtilities.h" @class SCSettings; @@ -41,8 +42,6 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu + (BOOL) currentBlockIsExpired; -+ (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL; -+ (BOOL) blockIsRunningInLegacyDictionary:(NSDictionary*)dict; // read and write saved block files + (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; @@ -54,13 +53,5 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu + (BOOL)errorIsAuthCanceled:(NSError*)err; -// migration methods -+ (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId; -+ (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID; -+ (BOOL)legacySettingsFoundForCurrentUser; -+ (NSDate*)legacyBlockEndDate; -+ (void)copyLegacySettingsToDefaults:(uid_t)controllingUID; -+ (void)copyLegacySettingsToDefaults; -+ (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID; @end diff --git a/Common/SCUtilities.m b/Common/SCUtilities.m index 9ee6b451..889ff7c6 100644 --- a/Common/SCUtilities.m +++ b/Common/SCUtilities.m @@ -8,7 +8,6 @@ #import "SCUtilities.h" #import "HelperCommon.h" #import "SCSettings.h" -#include @implementation SCUtilities @@ -169,14 +168,14 @@ + (BOOL)legacyBlockIsRunning { NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; - if ([SCUtilities legacyBlockIsRunningInSettingsFile: settingsFileURL]) { + if ([SCMigrationUtilities legacyBlockIsRunningInSettingsFile: settingsFileURL]) { return YES; } } } // nope? OK, how about a lock file from pre-3.0? - if ([[NSFileManager defaultManager] fileExistsAtPath: SelfControlLegacyLockFilePath]) { + if ([SCMigrationUtilities legacyLockFileExists]) { return YES; } @@ -186,15 +185,6 @@ + (BOOL)legacyBlockIsRunning { return NO; } -+ (BOOL)legacyBlockIsRunningInSettingsFile:(NSURL*)settingsFileURL { - NSDictionary* legacySettingsDict = [NSDictionary dictionaryWithContentsOfURL: settingsFileURL]; - - // if the file doesn't exist, there's definitely no block - if (legacySettingsDict == nil) return NO; - - return [SCUtilities blockIsRunningInLegacyDictionary: legacySettingsDict]; -} - // returns YES if the block should have expired active based on the specified end time (i.e. the end time is in the past), or NO otherwise + (BOOL)currentBlockIsExpired { // the block should be running if the end date hasn't arrived yet @@ -214,22 +204,6 @@ + (void) removeBlockFromSettings { [settings setValue: nil forKey: @"ActiveBlockAsWhitelist"]; } -+ (BOOL)blockIsRunningInLegacyDictionary:(NSDictionary*)dict { - if (dict == nil) return NO; - - NSDate* blockStartedDate = [dict objectForKey:@"BlockStartedDate"]; - BOOL blockIsRunningValue = [[dict objectForKey: @"BlockIsRunning"] boolValue]; - - // for v3.0-3.0.3: the block is running if the BlockIsRunning key is true - // super old legacy (pre-3.0): the block is running if BlockStartedDate exists and isn't equal to the default value - if (blockIsRunningValue || (blockStartedDate != nil && ![blockStartedDate isEqualToDate: [NSDate distantFuture]])) { - return YES; - } else { - return NO; - } -} - - + (BOOL)errorIsAuthCanceled:(NSError*)err { if (err == nil) return NO; @@ -344,236 +318,6 @@ + (NSError*)clearBrowserCaches { // migration functions -+ (NSString*)homeDirectoryForUid:(uid_t)uid { - struct passwd *pwd = getpwuid(uid); - return [NSString stringWithCString: pwd->pw_dir encoding: NSString.defaultCStringEncoding]; -} - -+ (NSString*)legacySecuredSettingsFilePathForUser:(uid_t)userId { - NSString* homeDir = [SCUtilities homeDirectoryForUid: userId]; - return [[NSString stringWithFormat: @"%@/Library/Preferences/%@", homeDir, SCSettings.settingsFileName] stringByExpandingTildeInPath]; -} - -// check all legacy settings (old secured settings, lockfile, old-school defaults) -// to see if there's anything there -+ (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID { - NSFileManager* fileMan = [NSFileManager defaultManager]; - NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: controllingUID]; - NSArray* defaultsHostBlacklist; - - if (geteuid() == 0 && controllingUID) { - // we're running as root, so get the defaults dictionary using our special function) - NSDictionary* defaultsDict = [SCUtilities defaultsDictForUser: controllingUID]; - defaultsHostBlacklist = defaultsDict[@"HostBlacklist"]; - } else { - // normal times, just use standard defaults - defaultsHostBlacklist = [[NSUserDefaults standardUserDefaults] objectForKey: @"HostBlacklist"]; - } - - return defaultsHostBlacklist || [fileMan fileExistsAtPath: legacySettingsPath] || [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]; -} -+ (BOOL)legacySettingsFoundForCurrentUser { - return [SCUtilities legacySettingsFoundForUser: getuid()]; -} - -+ (NSDate*)legacyBlockEndDate { - // if we're running this as a normal user (generally that means app/CLI), it's easy: just get the standard user defaults - // this method can't be run as root, it won't work - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; - NSDictionary* lockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLegacyLockFilePath]; - NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: getuid()]; - NSDictionary* settingsFromDisk = [NSDictionary dictionaryWithContentsOfFile: legacySettingsPath]; - - // if we have a v3.x settings dictionary, take from that - if (settingsFromDisk != nil && settingsFromDisk[@"BlockEndDate"] != nil) { - return settingsFromDisk[@"BlockEndDate"]; - } - - // otherwise, we can look in defaults or the lockfile, both from pre-3.x versions - // these would have BlockStartedDate + BlockDuration instead of BlockEndDate, so conversion is needed - NSDate* startDate = [defaults objectForKey: @"BlockStartedDate"]; - NSTimeInterval duration = [defaults floatForKey: @"BlockDuration"]; - - // if defaults didn't have valid values, try the lockfile - if (startDate == nil || [startDate timeIntervalSinceNow] >= 0 || duration <= 0) { - startDate = lockDict[@"BlockStartedDate"]; - duration = [lockDict[@"BlockStartedDate"] floatValue]; - } - if (startDate == nil || [startDate timeIntervalSinceNow] >= 0 || duration <= 0) { - // if still not, we give up! no end date found, so call it the past - return [NSDate distantPast]; - } - return [startDate dateByAddingTimeInterval: (duration * 60)]; -} - -// copies settings from legacy locations (user-based secured settings used from 3.0-3.0.3, -// or older defaults/lockfile used pre-3.0) to their modern destinations in NSUserDefaults. -// does NOT update any of the values in SCSettings, and does NOT clear out settings from anywhere -// that makes this safe to call anytime, includig while a block is running -+ (void)copyLegacySettingsToDefaults:(uid_t)controllingUID { - NSLog(@"Copying legacy settings to defaults..."); - BOOL runningAsRoot = (geteuid() == 0); - if (runningAsRoot && !controllingUID) { - // if we're running as root, but we didn't get a valid non-root controlling UID - // we don't really have anywhere to copy those legacy settings to, because root doesn't have defaults - NSLog(@"WARNING: Can't copy legacy settings to defaults, because SCSettings is being run as root and no controlling UID was sent."); - return; - } - if (!controllingUID) controllingUID = getuid(); - - NSDictionary* defaultDefaults = SCConstants.defaultUserDefaults; - // if we're running this as a normal user (generally that means app/CLI), it's easy: just get the standard user defaults - // if we're running this as root, we need to be given a UID target, then we imitate them to grab their defaults - NSUserDefaults* defaults; - if (runningAsRoot) { - seteuid(controllingUID); - defaults = [NSUserDefaults standardUserDefaults]; - [defaults addSuiteNamed: @"org.eyebeam.SelfControl"]; - [defaults synchronize]; - } else { - defaults = [NSUserDefaults standardUserDefaults]; - } - - NSDictionary* lockDict = [NSDictionary dictionaryWithContentsOfFile: SelfControlLegacyLockFilePath]; - - NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: controllingUID]; - NSDictionary* settingsFromDisk = [NSDictionary dictionaryWithContentsOfFile: legacySettingsPath]; - - // if we have a v3.x settings dictionary, copy what we can from that - if (settingsFromDisk != nil) { - NSLog(@"Migrating all settings from legacy secured settings file %@", legacySettingsPath); - - // we assume the settings from disk are newer / should override existing values - // UNLESS the user has set a default to its non-default value - - // we'll look at all the possible keys in defaults - some of them should really - // have never ended up in settings at any point, but shouldn't matter - for (NSString* key in [defaultDefaults allKeys]) { - id settingsValue = settingsFromDisk[key]; - id defaultsValue = [defaults objectForKey: key]; - - // we have a value from settings, and the defaults value is unset or equal to the default value - // so pull the value from settings in! - if (settingsValue != nil && (defaultsValue == nil || [defaultsValue isEqualTo: defaultDefaults[key]])) { - NSLog(@"Migrating keypair (%@, %@) from settings to defaults", key, settingsValue); - [defaults setObject: settingsValue forKey: key]; - } - } - - NSLog(@"Done migrating preferences from legacy secured settings to defaults!"); - } - - // if we're on a pre-3.0 version, we may need to migrate the blocklist from defaults or the lock dictionary - // the Blocklist attribute used to be named HostBlacklist, so needs a special migration - NSArray* blocklistInDefaults = [defaults arrayForKey: @"Blocklist"]; - // of course, don't overwrite if we already have a blocklist in today's defaults - if (blocklistInDefaults == nil || blocklistInDefaults.count == 0) { - if (lockDict != nil && lockDict[@"HostBlacklist"] != nil) { - [defaults setObject: lockDict[@"HostBlacklist"] forKey: @"Blocklist"]; - NSLog(@"Migrated blocklist from pre-3.0 lock dictionary: %@", lockDict[@"HostBlacklist"]); - } else if ([defaults objectForKey: @"HostBlacklist"] != nil) { - [defaults setObject: [defaults objectForKey: @"HostBlacklist"] forKey: @"Blocklist"]; - NSLog(@"Migrated blocklist from pre-3.0 legacy defaults: %@", [defaults objectForKey: @"HostBlacklist"]); - } - } - - // if we're running as root and imitated the user to get their defaults, we need to put things back in place when done - if (runningAsRoot) { - [NSUserDefaults resetStandardUserDefaults]; - seteuid(0); - } - - [SCSentry addBreadcrumb: @"Copied legacy settings to defaults successfully" category: @"settings"]; - NSLog(@"Done copying settings!"); -} - -+ (void)copyLegacySettingsToDefaults { - [SCUtilities copyLegacySettingsToDefaults: 0]; -} - -// We might have "legacy" block settings hiding in one of three places: -// - a "lock file" at /etc/SelfControl.lock (aka SelfControlLegacyLockFilePath) -// - the defaults system -// - a v3.x per-user secured settings file -// we should check for block settings in all of these places and get rid of them -+ (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID { - NSLog(@"Clearing legacy settings!"); - - BOOL runningAsRoot = (geteuid() == 0); - if (!runningAsRoot || !controllingUID) { - // if we're not running as root, or we didn't get a valid non-root controlling UID - // we won't have permissions to make this work. This method MUST be called with root perms - NSLog(@"ERROR: Can't clear legacy settings, because we aren't running as root."); - NSError* err = [SCErr errorWithCode: 701]; - [SCSentry captureError: err]; - return err; - } - - // if we're gonna clear settings, there can't be a block running anywhere. otherwise, we should wait! - if ([SCUtilities legacyBlockIsRunning]) { - NSLog(@"ERROR: Can't clear legacy settings because a block is ongoing!"); - NSError* err = [SCErr errorWithCode: 702]; - [SCSentry captureError: err]; - return err; - } - - NSFileManager* fileMan = [NSFileManager defaultManager]; - - // besides Blocklist and the values copied from the v3.0-3.0.3 settings file to defaults in copyLegacySettingsToDefaults - // we actually don't need to move anything else over! Why? - // 1. The other settings from 3.0-3.0.3 don't matter as long as a block isn't running (i.e. BlockIsRunning should be false - // and BlockEndDate shouldn't be set). - // 2. All of the non-block settings from pre-3.0 can stay in defaults, ahd BlockStartedDate should be false if no block running - // so all that's left is to clear out the legacy crap for good - - // if an error happens trying to clear any portion of the old settings, - // we'll remember it, log it, and return it, but still try to clear the rest (best-effort) - NSError* retErr = nil; - - // first, clear the pre-3.0 lock dictionary - if(![fileMan removeItemAtPath: SelfControlLegacyLockFilePath error: &retErr] && [fileMan fileExistsAtPath: SelfControlLegacyLockFilePath]) { - NSLog(@"WARNING: Could not remove legacy SelfControl lock file because of error: %@", retErr); - [SCSentry captureError: retErr]; - } - - // then, clear keys out of defaults which aren't used - // prepare defaults by imitating the appropriate user - seteuid(controllingUID); - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; - [defaults addSuiteNamed: @"org.eyebeam.SelfControl"]; - [defaults synchronize]; - NSArray* defaultsKeysToClear = @[ - @"BlockStartedDate", - @"BlockEndDate", - @"HostBlacklist" - ]; - for (NSString* key in defaultsKeysToClear) { - [defaults removeObjectForKey: key]; - } - [defaults synchronize]; - [NSUserDefaults resetStandardUserDefaults]; - seteuid(0); - - // clear all legacy per-user secured settings (v3.0-3.0.3) in every user's home folder - NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &retErr]; - if (homeDirectoryURLs != nil) { - for (NSURL* homeDirURL in homeDirectoryURLs) { - NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; - NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; - - if(![fileMan removeItemAtURL: settingsFileURL error: &retErr] && [fileMan fileExistsAtPath: settingsFileURL.path]) { - NSLog(@"WARNING: Could not remove legacy SelfControl settings file at URL %@ because of error: %@", settingsFileURL, retErr); - [SCSentry captureError: retErr]; - } - } - } - - // and that's it! note that we don't touch the modern SCSettings at all, and that's OK - it'll restart from scratch and be fine - [SCSentry addBreadcrumb: @"Cleared legacy settings successfully" category: @"settings"]; - NSLog(@"Cleared legacy settings!"); - - return retErr; -} @end diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 678294eb..9044edd9 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -70,9 +70,9 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray #import "HostImporter.h" #import "ThunderbirdPreferenceParser.h" -#import "SelfControlCommon.h" #import "SCSettings.h" // A subclass of NSWindowController created to manage the domain list (actually diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index 7f20db39..0bc101ac 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -13,6 +13,7 @@ #import "SCSettings.h" #import "HelperCommon.h" #import +#import "SCMigrationUtilities.h" #define LOG_FILE @"~/Documents/SelfControl-Killer.log" @@ -91,7 +92,7 @@ int main(int argc, char* argv[]) { SCSettings* settings = [SCSettings sharedSettings]; [log appendFormat: @"Current secured settings:\n\n:%@\n", settings.dictionaryRepresentation]; - NSString* legacySettingsPath = [SCUtilities legacySecuredSettingsFilePathForUser: controllingUID]; + NSString* legacySettingsPath = [SCMigrationUtilities legacySecuredSettingsFilePathForUser: controllingUID]; NSDictionary* legacySettingsDict = [NSDictionary dictionaryWithContentsOfFile: legacySettingsPath]; if (legacySettingsDict) { [log appendFormat: @"Legacy (3.0-3.0.3) secured settings:\n\n:%@\n", legacySettingsDict]; @@ -166,9 +167,9 @@ int main(int argc, char* argv[]) { [settings synchronizeSettings]; [log appendFormat: @"Reset all modern secured settings to default values.\n"]; - if ([SCUtilities legacySettingsFoundForUser: controllingUID]) { - [SCUtilities copyLegacySettingsToDefaults: controllingUID]; - [SCUtilities clearLegacySettingsForUser: controllingUID]; + if ([SCMigrationUtilities legacySettingsFoundForUser: controllingUID]) { + [SCMigrationUtilities copyLegacySettingsToDefaults: controllingUID]; + [SCMigrationUtilities clearLegacySettingsForUser: controllingUID]; [log appendFormat: @"Found, copied, and cleared legacy settings (v3.0-3.0.3)!\n"]; } else { [log appendFormat: @"No legacy settings (v3.0-3.0.3) found.\n"]; diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 4b705eb3..c0c49279 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -78,6 +78,13 @@ CB74D11F2480E55D002B2079 /* DaemonMain.m in Sources */ = {isa = PBXBuildFile; fileRef = CB74D1032480E4D9002B2079 /* DaemonMain.m */; }; CB74D1202480E566002B2079 /* SCDaemon.m in Sources */ = {isa = PBXBuildFile; fileRef = CB74D0FD2480E3E6002B2079 /* SCDaemon.m */; }; CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = CB8086D324837607004B88BD /* SCDaemonXPC.m */; }; + CB81A94825B7B5B5006956F7 /* SCMigrationUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = CB81A94625B7B5B5006956F7 /* SCMigrationUtilities.h */; }; + CB81A94925B7B5B5006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; + CB81A94A25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; + CB81A94B25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; + CB81A94C25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; + CB81A94D25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; + CB81A94E25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; CB850F3925130F5300EE2E2D /* NSString+IPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806516C237F10059C99A /* NSString+IPAddress.m */; }; CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */; }; CB90BF830F49F430006D202D /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; @@ -262,6 +269,8 @@ CB8086D224837607004B88BD /* SCDaemonXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonXPC.h; sourceTree = ""; }; CB8086D324837607004B88BD /* SCDaemonXPC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonXPC.m; sourceTree = ""; }; CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = org.eyebeam.selfcontrold.plist; sourceTree = ""; }; + CB81A94625B7B5B5006956F7 /* SCMigrationUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCMigrationUtilities.h; sourceTree = ""; }; + CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCMigrationUtilities.m; sourceTree = ""; }; CB850F3A2513185300EE2E2D /* SCDaemonUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonUtilities.h; sourceTree = ""; }; CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonUtilities.m; sourceTree = ""; }; CB90BF810F49F430006D202D /* HostImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostImporter.h; sourceTree = ""; }; @@ -313,7 +322,6 @@ CBCA912B1961384600AFD20C /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HelperCommon.h; sourceTree = ""; }; CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HelperCommon.m; sourceTree = ""; }; - CBD266E111ED84F700042CD8 /* SelfControlCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SelfControlCommon.h; sourceTree = ""; }; CBD4848219D75F6D0020F949 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sparkle.framework; sourceTree = ""; }; CBD4848519D7611F0020F949 /* Podfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; CBD4848619D7611F0020F949 /* TemplateIcon2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TemplateIcon2x.png; sourceTree = ""; }; @@ -591,6 +599,8 @@ CB1CA64C25ABA5990084A551 /* Common */ = { isa = PBXGroup; children = ( + CB81A94625B7B5B5006956F7 /* SCMigrationUtilities.h */, + CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */, CBB1731220F041F4007FCAE9 /* SCUtilities.h */, CBB1731320F041F4007FCAE9 /* SCUtilities.m */, CBADC27C25B22BC7000EE5BB /* SCSentry.h */, @@ -612,7 +622,6 @@ CB4294DF0F53D865008E10CA /* App Classes */ = { isa = PBXGroup; children = ( - CBD266E111ED84F700042CD8 /* SelfControlCommon.h */, CB25806416C237F10059C99A /* NSString+IPAddress.h */, CB25806516C237F10059C99A /* NSString+IPAddress.m */, CBE5C4090F4D4531003DB900 /* ButtonWithPopupMenu.h */, @@ -763,6 +772,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + CB81A94825B7B5B5006956F7 /* SCMigrationUtilities.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1239,6 +1249,7 @@ 8D11072D0486CEB800E47090 /* main.m in Sources */, CB1CA66525ABA6240084A551 /* SCXPCAuthorization.m in Sources */, CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */, + CB81A94925B7B5B5006956F7 /* SCMigrationUtilities.m in Sources */, CB1465B825B027E700130D2E /* SCErr.m in Sources */, CBB1731920F05C07007FCAE9 /* SCUtilities.m in Sources */, CB58948025B3FC6D00E9A5C0 /* HostFileBlocker.m in Sources */, @@ -1265,6 +1276,7 @@ CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */, CB114284222CD4F0004B7868 /* SCSettings.m in Sources */, CB0EEF7820FE49030024D27B /* SCUtilitiesTests.m in Sources */, + CB81A94D25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1288,6 +1300,7 @@ CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */, CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */, + CB81A94E25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */, CB1CA65125ABA5BB0084A551 /* SCXPCClient.m in Sources */, CB69C4EF25A3FD8A0030CFCD /* SCXPCAuthorization.m in Sources */, @@ -1308,6 +1321,7 @@ CB1465BA25B027E700130D2E /* SCErr.m in Sources */, CBB1731C20F05C0A007FCAE9 /* SCUtilities.m in Sources */, CBADC28025B22BC7000EE5BB /* SCSentry.m in Sources */, + CB81A94B25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1323,6 +1337,7 @@ CB9C812619CFBB5E00CDCAE1 /* BlockManager.m in Sources */, CB9C812519CFBB5500CDCAE1 /* HelperCommon.m in Sources */, CB9C812419CFBB4E00CDCAE1 /* HostFileBlocker.m in Sources */, + CB81A94C25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CB1CA64F25ABA5BB0084A551 /* SCXPCClient.m in Sources */, CB114283222CCF19004B7868 /* SCSettings.m in Sources */, CB9C812319CFBB4400CDCAE1 /* LaunchctlHelper.m in Sources */, @@ -1346,6 +1361,7 @@ CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */, CBC2F8580F4672FE00CF2A42 /* LaunchctlHelper.m in Sources */, CBB0AE2A0FA74566006229B3 /* HostFileBlocker.m in Sources */, + CB81A94A25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CBD266B011ED7D9C00042CD8 /* HelperCommon.m in Sources */, CB32D2A921902CB300B8CD68 /* SCSettings.m in Sources */, CB1CA65025ABA5BB0084A551 /* SCXPCClient.m in Sources */, diff --git a/SelfControlCommon.h b/SelfControlCommon.h deleted file mode 100644 index 5e701eab..00000000 --- a/SelfControlCommon.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// SelfControlCommon.h -// SelfControl -// -// Created by Charlie Stigler on 2/15/09. -// Copyright 2009 Eyebeam. - -// This file is part of SelfControl. -// -// SelfControl is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// This file holds variables, functions, includes, etc. that should be easily -// accessible from many different parts of SelfControl. - -#define SelfControlLegacyLockFilePath @"/etc/SelfControl.lock" -// #define DEBUG 0 diff --git a/SelfControlTests/SCUtilitiesTests.m b/SelfControlTests/SCUtilitiesTests.m index c73284f8..ee60a491 100644 --- a/SelfControlTests/SCUtilitiesTests.m +++ b/SelfControlTests/SCUtilitiesTests.m @@ -167,14 +167,14 @@ - (void) testModernBlockDetection { - (void) testLegacyBlockDetection { // test blockIsRunningInLegacyDictionary // the block is "running" even if it's expired, since it hasn't been removed - XCTAssert([SCUtilities blockIsRunningInLegacyDictionary: activeBlockLegacyDict]); - XCTAssert([SCUtilities blockIsRunningInLegacyDictionary: expiredBlockLegacyDict]); - XCTAssert(![SCUtilities blockIsRunningInLegacyDictionary: noBlockLegacyDict]); - XCTAssert(![SCUtilities blockIsRunningInLegacyDictionary: noBlockLegacyDict2]); - XCTAssert([SCUtilities blockIsRunningInLegacyDictionary: futureStartDateLegacyDict]); - XCTAssert([SCUtilities blockIsRunningInLegacyDictionary: negativeBlockDurationLegacyDict]); // negative still might be running? - XCTAssert([SCUtilities blockIsRunningInLegacyDictionary: veryLongBlockLegacyDict]); - XCTAssert(![SCUtilities blockIsRunningInLegacyDictionary: emptyLegacyDict]); + XCTAssert([SCMigrationUtilities blockIsRunningInLegacyDictionary: activeBlockLegacyDict]); + XCTAssert([SCMigrationUtilities blockIsRunningInLegacyDictionary: expiredBlockLegacyDict]); + XCTAssert(![SCMigrationUtilities blockIsRunningInLegacyDictionary: noBlockLegacyDict]); + XCTAssert(![SCMigrationUtilities blockIsRunningInLegacyDictionary: noBlockLegacyDict2]); + XCTAssert([SCMigrationUtilities blockIsRunningInLegacyDictionary: futureStartDateLegacyDict]); + XCTAssert([SCMigrationUtilities blockIsRunningInLegacyDictionary: negativeBlockDurationLegacyDict]); // negative still might be running? + XCTAssert([SCMigrationUtilities blockIsRunningInLegacyDictionary: veryLongBlockLegacyDict]); + XCTAssert(![SCMigrationUtilities blockIsRunningInLegacyDictionary: emptyLegacyDict]); } @end diff --git a/TimerWindowController.h b/TimerWindowController.h index 01263975..36ec5a11 100755 --- a/TimerWindowController.h +++ b/TimerWindowController.h @@ -22,7 +22,6 @@ #import #import "AppController.h" -#import "SelfControlCommon.h" // A subclass of NSWindowController created to manage the floating timer window // which tells the user how much time remains in the block. diff --git a/TimerWindowController.m b/TimerWindowController.m index 05597183..61959f1a 100755 --- a/TimerWindowController.m +++ b/TimerWindowController.m @@ -80,7 +80,7 @@ - (void)awakeFromNib { blockEndingDate_ = [settings_ valueForKey: @"BlockEndDate"]; } else { // legacy block! - blockEndingDate_ = [SCUtilities legacyBlockEndDate]; + blockEndingDate_ = [SCMigrationUtilities legacyBlockEndDate]; // if it's a legacy block, we will disable some features // since it's too difficult to get these working across versions. @@ -286,7 +286,7 @@ - (void) blockEndDateUpdated { } else { // legacy block! - blockEndingDate_ = [SCUtilities legacyBlockEndDate]; + blockEndingDate_ = [SCMigrationUtilities legacyBlockEndDate]; } [self performSelectorOnMainThread: @selector(updateTimerDisplay:) withObject:nil waitUntilDone: YES]; From 9f66cd26e53481640ec8a8a5f350a501a1f74662 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 18:11:04 -0800 Subject: [PATCH 56/72] Split SCUtilities into several utility files --- AppController.m | 24 ++-- Common/HelperCommon.h | 1 - Common/HelperCommon.m | 5 +- Common/SCBlockFileReaderWriter.h | 20 ++++ Common/SCBlockFileReaderWriter.m | 49 ++++++++ Common/SCSettings.m | 7 +- Common/SCUtilities.h | 57 --------- Common/SCXPCClient.m | 9 +- Common/Utility/SCBlockUtilities.h | 25 ++++ Common/Utility/SCBlockUtilities.m | 70 +++++++++++ Common/{ => Utility}/SCMigrationUtilities.h | 0 Common/{ => Utility}/SCMigrationUtilities.m | 7 +- Common/Utility/SCMiscUtilities.h | 29 +++++ .../SCMiscUtilities.m} | 111 +----------------- Common/Utility/SCUtility.h | 15 +++ Daemon/SCDaemon.m | 5 +- Daemon/SCDaemonBlockMethods.m | 16 +-- Daemon/SCDaemonUtilities.m | 1 - Daemon/SCDaemonXPC.m | 7 +- DomainListWindowController.m | 3 +- SCError.strings | 2 +- SelfControl.xcodeproj/project.pbxproj | 90 ++++++++++---- .../{SCUtilitiesTests.m => SCUtilityTests.m} | 56 ++++----- SelfControl_Prefix.pch | 2 + TimerWindowController.m | 9 +- cli-main.m | 10 +- 26 files changed, 360 insertions(+), 270 deletions(-) create mode 100644 Common/SCBlockFileReaderWriter.h create mode 100644 Common/SCBlockFileReaderWriter.m delete mode 100644 Common/SCUtilities.h create mode 100644 Common/Utility/SCBlockUtilities.h create mode 100644 Common/Utility/SCBlockUtilities.m rename Common/{ => Utility}/SCMigrationUtilities.h (100%) rename Common/{ => Utility}/SCMigrationUtilities.m (98%) create mode 100644 Common/Utility/SCMiscUtilities.h rename Common/{SCUtilities.m => Utility/SCMiscUtilities.m} (67%) create mode 100644 Common/Utility/SCUtility.h rename SelfControlTests/{SCUtilitiesTests.m => SCUtilityTests.m} (74%) diff --git a/AppController.m b/AppController.m index f3d5a140..36b793e8 100755 --- a/AppController.m +++ b/AppController.m @@ -25,13 +25,13 @@ #import "PreferencesGeneralViewController.h" #import "PreferencesAdvancedViewController.h" #import "SCTimeIntervalFormatter.h" -#import "SCUtilities.h" #import #import #import "SCSettings.h" #import #import "SCXPCClient.h" #import "HostFileBlocker.h" +#import "SCBlockFileReaderWriter.h" @interface AppController () {} @@ -322,7 +322,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // 3. There's a daemon but it's an old version, and should be replaced. // in any case, let's go try to reinstall the daemon // (we debounce this call so it happens only once, after the connection has been invalidated for an extended period) - if ([SCUtilities modernBlockIsRunning]) { + if ([SCBlockUtilities modernBlockIsRunning]) { [NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: NO block:^(NSTimer * _Nonnull timer) { [self.xpc getVersion:^(NSString * _Nonnull daemonVersion, NSError * _Nonnull error) { if (error == nil) { @@ -404,7 +404,7 @@ - (void)reinstallDaemon { NSLog(@"Retrying helper tool connection..."); [self.xpc performSelectorOnMainThread: @selector(connectToHelperTool) withObject: nil waitUntilDone: YES]; } else { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { NSLog(@"ERROR: Reinstalling daemon failed with error %@", error); [NSApp presentError: error]; } @@ -417,7 +417,7 @@ - (BOOL)blockIsRunning { // also, importantly, if we find a block still going in the hosts file // that way if this happens, the user will still see the timer window - // which will let them manually clear the remaining block info after 10 seconds - return [SCUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]; + return [SCBlockUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]; } - (IBAction)showDomainList:(id)sender { @@ -471,7 +471,7 @@ - (void)addToBlockList:(NSString*)host lock:(NSLock*)lock { // Note we RETRIEVE the latest list from settings (ActiveBlocklist), but we SET the new list in defaults // since the helper daemon should be the only one changing ActiveBlocklist NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy]; - NSArray* cleanedEntries = [SCUtilities cleanBlocklistEntry: host]; + NSArray* cleanedEntries = [SCMiscUtilities cleanBlocklistEntry: host]; if (cleanedEntries.count == 0) return; @@ -579,7 +579,7 @@ - (void)installBlock { [self refreshUserInterface]; [self.xpc installDaemon:^(NSError * _Nonnull error) { if (error != nil) { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { [NSApp performSelectorOnMainThread: @selector(presentError:) withObject: error waitUntilDone: YES]; @@ -617,7 +617,7 @@ - (void)installBlock { } reply:^(NSError * _Nonnull error) { if (error != nil ) { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { [NSApp performSelectorOnMainThread: @selector(presentError:) withObject: error waitUntilDone: YES]; @@ -656,7 +656,7 @@ - (void)updateActiveBlocklist:(NSLock*)lockToUse { [self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES]; if (error != nil) { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { [NSApp performSelectorOnMainThread: @selector(presentError:) withObject: error waitUntilDone: YES]; @@ -696,7 +696,7 @@ - (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToA [self.xpc refreshConnectionAndRun:^{ // Before we try to extend the block, make sure the block time didn't run out (or is about to run out) in the meantime - if ([SCUtilities currentBlockIsExpired] || [oldBlockEndDate timeIntervalSinceNow] < 1) { + if ([SCBlockUtilities currentBlockIsExpired] || [oldBlockEndDate timeIntervalSinceNow] < 1) { // we're done, or will be by the time we get to it! so just let it expire. they can restart it. [lockToUse unlock]; return; @@ -712,7 +712,7 @@ - (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToA waitUntilDone: YES]; if (error != nil) { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { [NSApp performSelectorOnMainThread: @selector(presentError:) withObject: error waitUntilDone: YES]; @@ -740,7 +740,7 @@ - (IBAction)save:(id)sender { /* if successful, save file under designated name */ if (runResult == NSOKButton) { NSString* errDescription; - [SCUtilities writeBlocklistToFileURL: sp.URL + [SCBlockFileReaderWriter writeBlocklistToFileURL: sp.URL blockInfo: @{ @"Blocklist": [defaults_ arrayForKey: @"Blocklist"], @"BlockAsWhitelist": [defaults_ objectForKey: @"BlockAsWhitelist"] @@ -768,7 +768,7 @@ - (IBAction)open:(id)sender { long result = [oPanel runModal]; if (result == NSOKButton) { if([oPanel.URLs count] > 0) { - NSDictionary* settingsFromFile = [SCUtilities readBlocklistFromFile: oPanel.URLs[0]]; + NSDictionary* settingsFromFile = [SCBlockFileReaderWriter readBlocklistFromFile: oPanel.URLs[0]]; if (settingsFromFile != nil) { [defaults_ setObject: settingsFromFile[@"Blocklist"] forKey: @"Blocklist"]; diff --git a/Common/HelperCommon.h b/Common/HelperCommon.h index 6ab8d9eb..b13b553e 100644 --- a/Common/HelperCommon.h +++ b/Common/HelperCommon.h @@ -28,7 +28,6 @@ #import #import #import "HostFileBlocker.h" -#import "SCUtilities.h" #import "SCSettings.h" // Reads the domain block list from the settings for SelfControl, and adds deny diff --git a/Common/HelperCommon.m b/Common/HelperCommon.m index 8fbc3e81..75bccd42 100644 --- a/Common/HelperCommon.m +++ b/Common/HelperCommon.m @@ -9,7 +9,6 @@ #include "HelperCommon.h" #include "BlockManager.h" -#import "SCUtilities.h" #import "SCSettings.h" #import @@ -106,7 +105,7 @@ void clearCachesIfRequested() { return; } - NSError* err = [SCUtilities clearBrowserCaches]; + NSError* err = [SCMiscUtilities clearBrowserCaches]; if (err) { NSLog(@"WARNING: Error clearing browser caches: %@", err); [SCSentry captureError: err]; @@ -141,7 +140,7 @@ void clearOSDNSCache() { void removeBlock() { SCSettings* settings = [SCSettings sharedSettings]; - [SCUtilities removeBlockFromSettings]; + [SCBlockUtilities removeBlockFromSettings]; removeRulesFromFirewall(); // always synchronize settings ASAP after removing a block to let everybody else know diff --git a/Common/SCBlockFileReaderWriter.h b/Common/SCBlockFileReaderWriter.h new file mode 100644 index 00000000..c10ce16a --- /dev/null +++ b/Common/SCBlockFileReaderWriter.h @@ -0,0 +1,20 @@ +// +// SCBlockFileReaderWriter.h +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SCBlockFileReaderWriter : NSObject + +// read and write saved block files ++ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; ++ (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Common/SCBlockFileReaderWriter.m b/Common/SCBlockFileReaderWriter.m new file mode 100644 index 00000000..55a6fa7e --- /dev/null +++ b/Common/SCBlockFileReaderWriter.m @@ -0,0 +1,49 @@ +// +// SCBlockFileReaderWriter.m +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import "SCBlockFileReaderWriter.h" + +@implementation SCBlockFileReaderWriter + ++ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef { + NSDictionary* saveDict = @{@"HostBlacklist": [blockInfo objectForKey: @"Blocklist"], + @"BlockAsWhitelist": [blockInfo objectForKey: @"BlockAsWhitelist"]}; + + NSString* saveDataErr; + NSData* saveData = [NSPropertyListSerialization dataFromPropertyList: saveDict format: NSPropertyListBinaryFormat_v1_0 errorDescription: &saveDataErr]; + if (saveDataErr != nil) { + *errDescriptionRef = saveDataErr; + return NO; + } + + if (![saveData writeToURL: targetFileURL atomically: YES]) { + NSLog(@"ERROR: Failed to write blocklist to URL %@", targetFileURL); + return NO; + } + + // for prettiness sake, attempt to hide the file extension + NSDictionary* attribs = @{NSFileExtensionHidden: @YES}; + [[NSFileManager defaultManager] setAttributes: attribs ofItemAtPath: [targetFileURL path] error: NULL]; + + return YES; +} + ++ (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL { + NSDictionary* openedDict = [NSDictionary dictionaryWithContentsOfURL: fileURL]; + + if (openedDict == nil || openedDict[@"HostBlacklist"] == nil || openedDict[@"BlockAsWhitelist"] == nil) { + NSLog(@"ERROR: Could not read a valid block from file %@", fileURL); + return nil; + } + + return @{ + @"Blocklist": openedDict[@"HostBlacklist"], + @"BlockAsWhitelist": openedDict[@"BlockAsWhitelist"] + }; +} + +@end diff --git a/Common/SCSettings.m b/Common/SCSettings.m index f025105f..2ad85015 100644 --- a/Common/SCSettings.m +++ b/Common/SCSettings.m @@ -8,7 +8,6 @@ #import "SCSettings.h" #include #import -#import "SCUtilities.h" #import #ifndef TESTING @@ -559,10 +558,12 @@ - (void)onSettingChanged:(NSNotification*)note { } dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); double throttleSecs = 0.25f; - self.debouncedChangeTimer = CreateDebounceDispatchTimer(throttleSecs, queue, ^{ + self.debouncedChangeTimer = [SCMiscUtilities createDebounceDispatchTimer: throttleSecs + queue: queue + block: ^{ NSLog(@"Syncing settings due to propagated changes"); [self synchronizeSettings]; - }); + }]; } - (void)resetAllSettingsToDefaults { diff --git a/Common/SCUtilities.h b/Common/SCUtilities.h deleted file mode 100644 index e7585ff4..00000000 --- a/Common/SCUtilities.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// SCBlockDateUtilities.h -// SelfControl -// -// Created by Charles Stigler on 07/07/2018. -// - -#import -#import "SCMigrationUtilities.h" - -@class SCSettings; - -// Holds utility methods for use throughout SelfControl - - -@interface SCUtilities : NSObject - -dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queue_t queue, dispatch_block_t block); - -+ (NSArray*) cleanBlocklistEntry:(NSString*)rawEntry; - -/* BLOCK SETTING METHODS */ - -// The point of these methods is basically to abstract out whether we're using blockStartedDate (the old system) -// or blockEndDate (the new system) for tracking blocks in settings/lockfile. We want to be backwards-compatible for a while so people -// who upgrade mid-block (foolishly!) have a better chance of surviving and we don't bork their stuff. -// eventually, another way to do this would just be to convert all blockStartedDates to blockEndDates on launch, -// but that sounds risky (updating lock files is not guaranteed) and this seems safer for now... - -+ (NSDictionary*) defaultsDictForUser:(uid_t)controllingUID; - -// Main app functions taking NSUserDefaults and SCSettings - -+ (void) removeBlockFromSettings; - -// Helper tool functions dealing with dictionaries and setDefaultsValue helper - -// uses the below methods as well as filesystem checks to see if the block is REALLY running or not -+ (BOOL)anyBlockIsRunning; -+ (BOOL)modernBlockIsRunning; -+ (BOOL)legacyBlockIsRunning; - -+ (BOOL) currentBlockIsExpired; - - -// read and write saved block files -+ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; -+ (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL; - -+ (NSArray*)allUserHomeDirectoryURLs:(NSError**)errPtr; - -+ (NSError*)clearBrowserCaches; - -+ (BOOL)errorIsAuthCanceled:(NSError*)err; - - -@end diff --git a/Common/SCXPCClient.m b/Common/SCXPCClient.m index 7e2f5336..d451a953 100644 --- a/Common/SCXPCClient.m +++ b/Common/SCXPCClient.m @@ -9,7 +9,6 @@ #import "SCDaemonProtocol.h" #import #import "SCXPCAuthorization.h" -#import "SCUtilities.h" #import "SCErr.h" @interface SCXPCClient () { @@ -192,7 +191,7 @@ - (void)installDaemon:(void(^)(NSError*))callback { NSLog(@"WARNING: Authorized installation of selfcontrold returned failure status code %d and error %@", (int)status, error); NSError* err = [SCErr errorWithCode: 500 subDescription: error.localizedDescription]; - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { [SCSentry captureError: err]; } @@ -275,7 +274,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist reply:(void(^)(NSError [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlocklist: newBlocklist authorization: self.authorization reply:^(NSError* error) { - if (error != nil && ![SCUtilities errorIsAuthCanceled: error]) { + if (error != nil && ![SCMiscUtilities errorIsAuthCanceled: error]) { NSLog(@"Blocklist update failed with error = %@\n", error); [SCSentry captureError: error]; } @@ -319,7 +318,7 @@ - (void)updateBlockEndDate:(NSDate*)newEndDate reply:(void(^)(NSError* error))re [SCSentry captureError: proxyError]; reply(proxyError); }] updateBlockEndDate: newEndDate authorization: self.authorization reply:^(NSError* error) { - if (error != nil && ![SCUtilities errorIsAuthCanceled: error]) { + if (error != nil && ![SCMiscUtilities errorIsAuthCanceled: error]) { NSLog(@"Block end date update failed with error = %@\n", error); [SCSentry captureError: error]; } diff --git a/Common/Utility/SCBlockUtilities.h b/Common/Utility/SCBlockUtilities.h new file mode 100644 index 00000000..ec652fb1 --- /dev/null +++ b/Common/Utility/SCBlockUtilities.h @@ -0,0 +1,25 @@ +// +// SCBlockUtilities.h +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SCBlockUtilities : NSObject + +// uses the below methods as well as filesystem checks to see if the block is REALLY running or not ++ (BOOL)anyBlockIsRunning; ++ (BOOL)modernBlockIsRunning; ++ (BOOL)legacyBlockIsRunning; + ++ (BOOL)currentBlockIsExpired; + ++ (void)removeBlockFromSettings; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Common/Utility/SCBlockUtilities.m b/Common/Utility/SCBlockUtilities.m new file mode 100644 index 00000000..91b8d52e --- /dev/null +++ b/Common/Utility/SCBlockUtilities.m @@ -0,0 +1,70 @@ +// +// SCBlockUtilities.m +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import "SCBlockUtilities.h" + +@implementation SCBlockUtilities + ++ (BOOL)anyBlockIsRunning { + BOOL blockIsRunning = [SCBlockUtilities modernBlockIsRunning] || [SCBlockUtilities legacyBlockIsRunning]; + + return blockIsRunning; +} + ++ (BOOL)modernBlockIsRunning { + SCSettings* settings = [SCSettings sharedSettings]; + + return [settings boolForKey: @"BlockIsRunning"]; +} + ++ (BOOL)legacyBlockIsRunning { + // first see if there's a legacy settings file from v3.x + // which could be in any user's home folder + NSError* homeDirErr = nil; + NSArray* homeDirectoryURLs = [SCMiscUtilities allUserHomeDirectoryURLs: &homeDirErr]; + if (homeDirectoryURLs != nil) { + for (NSURL* homeDirURL in homeDirectoryURLs) { + NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; + NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; + + if ([SCMigrationUtilities legacyBlockIsRunningInSettingsFile: settingsFileURL]) { + return YES; + } + } + } + + // nope? OK, how about a lock file from pre-3.0? + if ([SCMigrationUtilities legacyLockFileExists]) { + return YES; + } + + // we don't check defaults anymore, though pre-3.0 blocks did + // have data stored there. That should be covered by the lockfile anyway + + return NO; +} + +// returns YES if the block should have expired active based on the specified end time (i.e. the end time is in the past), or NO otherwise ++ (BOOL)currentBlockIsExpired { + // the block should be running if the end date hasn't arrived yet + SCSettings* settings = [SCSettings sharedSettings]; + if ([[settings valueForKey: @"BlockEndDate"] timeIntervalSinceNow] > 0) { + return NO; + } else { + return YES; + } +} + ++ (void) removeBlockFromSettings { + SCSettings* settings = [SCSettings sharedSettings]; + [settings setValue: @NO forKey: @"BlockIsRunning"]; + [settings setValue: nil forKey: @"BlockEndDate"]; + [settings setValue: nil forKey: @"ActiveBlocklist"]; + [settings setValue: nil forKey: @"ActiveBlockAsWhitelist"]; +} + +@end diff --git a/Common/SCMigrationUtilities.h b/Common/Utility/SCMigrationUtilities.h similarity index 100% rename from Common/SCMigrationUtilities.h rename to Common/Utility/SCMigrationUtilities.h diff --git a/Common/SCMigrationUtilities.m b/Common/Utility/SCMigrationUtilities.m similarity index 98% rename from Common/SCMigrationUtilities.m rename to Common/Utility/SCMigrationUtilities.m index 65003d87..15f088eb 100644 --- a/Common/SCMigrationUtilities.m +++ b/Common/Utility/SCMigrationUtilities.m @@ -10,7 +10,6 @@ #import "SCMigrationUtilities.h" #import #import "SCSettings.h" -#import "SCUtilities.h" @implementation SCMigrationUtilities @@ -33,7 +32,7 @@ + (BOOL)legacySettingsFoundForUser:(uid_t)controllingUID { if (geteuid() == 0 && controllingUID) { // we're running as root, so get the defaults dictionary using our special function) - NSDictionary* defaultsDict = [SCUtilities defaultsDictForUser: controllingUID]; + NSDictionary* defaultsDict = [SCMiscUtilities defaultsDictForUser: controllingUID]; defaultsHostBlacklist = defaultsDict[@"HostBlacklist"]; } else { // normal times, just use standard defaults @@ -208,7 +207,7 @@ + (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID { } // if we're gonna clear settings, there can't be a block running anywhere. otherwise, we should wait! - if ([SCUtilities legacyBlockIsRunning]) { + if ([SCBlockUtilities legacyBlockIsRunning]) { NSLog(@"ERROR: Can't clear legacy settings because a block is ongoing!"); NSError* err = [SCErr errorWithCode: 702]; [SCSentry captureError: err]; @@ -253,7 +252,7 @@ + (NSError*)clearLegacySettingsForUser:(uid_t)controllingUID { seteuid(0); // clear all legacy per-user secured settings (v3.0-3.0.3) in every user's home folder - NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &retErr]; + NSArray* homeDirectoryURLs = [SCMiscUtilities allUserHomeDirectoryURLs: &retErr]; if (homeDirectoryURLs != nil) { for (NSURL* homeDirURL in homeDirectoryURLs) { NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; diff --git a/Common/Utility/SCMiscUtilities.h b/Common/Utility/SCMiscUtilities.h new file mode 100644 index 00000000..a59b976a --- /dev/null +++ b/Common/Utility/SCMiscUtilities.h @@ -0,0 +1,29 @@ +// +// SCMiscUtilities.h +// SelfControl +// +// Created by Charles Stigler on 07/07/2018. +// + +#import +#import "SCMigrationUtilities.h" + +// Holds utility methods for use throughout SelfControl + + +@interface SCMiscUtilities : NSObject + ++ (dispatch_source_t)createDebounceDispatchTimer:(double) debounceTime queue:(dispatch_queue_t)queue block:(dispatch_block_t)block; + ++ (NSArray*) cleanBlocklistEntry:(NSString*)rawEntry; + ++ (NSDictionary*) defaultsDictForUser:(uid_t)controllingUID; + ++ (NSArray*)allUserHomeDirectoryURLs:(NSError**)errPtr; + ++ (NSError*)clearBrowserCaches; + ++ (BOOL)errorIsAuthCanceled:(NSError*)err; + + +@end diff --git a/Common/SCUtilities.m b/Common/Utility/SCMiscUtilities.m similarity index 67% rename from Common/SCUtilities.m rename to Common/Utility/SCMiscUtilities.m index 889ff7c6..551fb0d1 100644 --- a/Common/SCUtilities.m +++ b/Common/Utility/SCMiscUtilities.m @@ -1,18 +1,17 @@ // -// SCBlockDateUtilities.m +// SCMiscUtilities.m // SelfControl // // Created by Charles Stigler on 07/07/2018. // -#import "SCUtilities.h" #import "HelperCommon.h" #import "SCSettings.h" -@implementation SCUtilities +@implementation SCMiscUtilities // copied from stevenojo's GitHub snippet: https://gist.github.com/stevenojo/e1dcc2b3e2fd4ed1f411eef88e254cb0 -dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queue_t queue, dispatch_block_t block) { ++ (dispatch_source_t)createDebounceDispatchTimer:(double)debounceTime queue:(dispatch_queue_t)queue block:(dispatch_block_t)block { dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); if (timer) { @@ -40,7 +39,7 @@ dispatch_source_t CreateDebounceDispatchTimer(double debounceTime, dispatch_queu NSMutableArray* returnArr = [NSMutableArray new]; for (NSString* splitEntry in splitEntries) { // recursion makes the rest of the code prettier - NSArray* cleanedSubEntries = [SCUtilities cleanBlocklistEntry: splitEntry]; + NSArray* cleanedSubEntries = [SCMiscUtilities cleanBlocklistEntry: splitEntry]; [returnArr addObjectsFromArray: cleanedSubEntries]; } return returnArr; @@ -146,64 +145,6 @@ + (NSDictionary*) defaultsDictForUser:(uid_t) controllingUID { return dictValue; } -+ (BOOL)anyBlockIsRunning { - BOOL blockIsRunning = [SCUtilities modernBlockIsRunning] || [self legacyBlockIsRunning]; - - return blockIsRunning; -} - -+ (BOOL)modernBlockIsRunning { - SCSettings* settings = [SCSettings sharedSettings]; - - return [settings boolForKey: @"BlockIsRunning"]; -} - -+ (BOOL)legacyBlockIsRunning { - // first see if there's a legacy settings file from v3.x - // which could be in any user's home folder - NSError* homeDirErr = nil; - NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &homeDirErr]; - if (homeDirectoryURLs != nil) { - for (NSURL* homeDirURL in homeDirectoryURLs) { - NSString* relativeSettingsPath = [NSString stringWithFormat: @"/Library/Preferences/%@", SCSettings.settingsFileName]; - NSURL* settingsFileURL = [homeDirURL URLByAppendingPathComponent: relativeSettingsPath isDirectory: NO]; - - if ([SCMigrationUtilities legacyBlockIsRunningInSettingsFile: settingsFileURL]) { - return YES; - } - } - } - - // nope? OK, how about a lock file from pre-3.0? - if ([SCMigrationUtilities legacyLockFileExists]) { - return YES; - } - - // we don't check defaults anymore, though pre-3.0 blocks did - // have data stored there. That should be covered by the lockfile anyway - - return NO; -} - -// returns YES if the block should have expired active based on the specified end time (i.e. the end time is in the past), or NO otherwise -+ (BOOL)currentBlockIsExpired { - // the block should be running if the end date hasn't arrived yet - SCSettings* settings = [SCSettings sharedSettings]; - if ([[settings valueForKey: @"BlockEndDate"] timeIntervalSinceNow] > 0) { - return NO; - } else { - return YES; - } -} - -+ (void) removeBlockFromSettings { - SCSettings* settings = [SCSettings sharedSettings]; - [settings setValue: @NO forKey: @"BlockIsRunning"]; - [settings setValue: nil forKey: @"BlockEndDate"]; - [settings setValue: nil forKey: @"ActiveBlocklist"]; - [settings setValue: nil forKey: @"ActiveBlockAsWhitelist"]; -} - + (BOOL)errorIsAuthCanceled:(NSError*)err { if (err == nil) return NO; @@ -217,43 +158,6 @@ + (BOOL)errorIsAuthCanceled:(NSError*)err { return NO; } -+ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef { - NSDictionary* saveDict = @{@"HostBlacklist": [blockInfo objectForKey: @"Blocklist"], - @"BlockAsWhitelist": [blockInfo objectForKey: @"BlockAsWhitelist"]}; - - NSString* saveDataErr; - NSData* saveData = [NSPropertyListSerialization dataFromPropertyList: saveDict format: NSPropertyListBinaryFormat_v1_0 errorDescription: &saveDataErr]; - if (saveDataErr != nil) { - *errDescriptionRef = saveDataErr; - return NO; - } - - if (![saveData writeToURL: targetFileURL atomically: YES]) { - NSLog(@"ERROR: Failed to write blocklist to URL %@", targetFileURL); - return NO; - } - - // for prettiness sake, attempt to hide the file extension - NSDictionary* attribs = @{NSFileExtensionHidden: @YES}; - [[NSFileManager defaultManager] setAttributes: attribs ofItemAtPath: [targetFileURL path] error: NULL]; - - return YES; -} - -+ (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL { - NSDictionary* openedDict = [NSDictionary dictionaryWithContentsOfURL: fileURL]; - - if (openedDict == nil || openedDict[@"HostBlacklist"] == nil || openedDict[@"BlockAsWhitelist"] == nil) { - NSLog(@"ERROR: Could not read a valid block from file %@", fileURL); - return nil; - } - - return @{ - @"Blocklist": openedDict[@"HostBlacklist"], - @"BlockAsWhitelist": openedDict[@"BlockAsWhitelist"] - }; -} - + (NSArray*)allUserHomeDirectoryURLs:(NSError**)errPtr { NSError* retErr = nil; NSFileManager* fileManager = [NSFileManager defaultManager]; @@ -281,7 +185,7 @@ + (NSError*)clearBrowserCaches { NSFileManager* fileManager = [NSFileManager defaultManager]; NSError* homeDirErr = nil; - NSArray* homeDirectoryURLs = [SCUtilities allUserHomeDirectoryURLs: &homeDirErr]; + NSArray* homeDirectoryURLs = [SCMiscUtilities allUserHomeDirectoryURLs: &homeDirErr]; if (homeDirectoryURLs == nil) return homeDirErr; NSArray* cacheDirPathComponents = @[ @@ -315,9 +219,4 @@ + (NSError*)clearBrowserCaches { return nil; } - -// migration functions - - - @end diff --git a/Common/Utility/SCUtility.h b/Common/Utility/SCUtility.h new file mode 100644 index 00000000..6f67ca34 --- /dev/null +++ b/Common/Utility/SCUtility.h @@ -0,0 +1,15 @@ +// +// SCUtility.h +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#ifndef SCUtility_h +#define SCUtility_h + +#import "SCMigrationUtilities.h" +#import "SCBlockUtilities.h" +#import "SCMiscUtilities.h" + +#endif /* SCUtility_h */ diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 1808f929..70dfb1a2 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -9,7 +9,6 @@ #import "SCDaemonProtocol.h" #import "SCDaemonXPC.h" #import"SCDaemonBlockMethods.h" -#import "SCUtilities.h" #import "HostFileBlocker.h" #import "SCDaemonUtilities.h" @@ -60,7 +59,7 @@ - (void)start { // we do NOT run checkup if there's no block, because it can result // in the daemon actually unloading itself before the app has a chance // to start the block - if ([SCUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]) { + if ([SCBlockUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]) { [self startCheckupTimer]; } @@ -106,7 +105,7 @@ - (void)startInactivityTimer { if ([[NSDate date] timeIntervalSinceDate: self.lastActivityDate] > INACTIVITY_LIMIT_SECS) { // if we're inactive but also there's a block running, that's a bad thing // start the checkups going again - unclear why they would've stopped - if ([SCUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]) { + if ([SCBlockUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile]) { [self startCheckupTimer]; [SCDaemonBlockMethods checkupBlock]; return; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 9044edd9..79ac58d0 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -59,7 +59,7 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData } [SCSentry addBreadcrumb: @"Daemon method updateBlocklist called" category: @"daemon"]; - if ([SCUtilities legacyBlockIsRunning]) { + if ([SCBlockUtilities legacyBlockIsRunning]) { NSLog(@"ERROR: Can't update blocklist because a legacy block is running"); NSError* err = [SCErr errorWithCode: 303]; [SCSentry captureError: err]; @@ -141,7 +141,7 @@ + (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData [self.daemonMethodLock unlock]; return; } - if (![SCUtilities modernBlockIsRunning]) { + if (![SCBlockUtilities modernBlockIsRunning]) { NSLog(@"ERROR: Can't update blocklist since block isn't running"); NSError* err = [SCErr errorWithCode: 304]; [SCSentry captureError: err]; @@ -204,7 +204,7 @@ + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData [SCSentry addBreadcrumb: @"Daemon method updateBlockEndDate called" category: @"daemon"]; - if ([SCUtilities legacyBlockIsRunning]) { + if ([SCBlockUtilities legacyBlockIsRunning]) { NSLog(@"ERROR: Can't update block end date because a legacy block is running"); NSError* err = [SCErr errorWithCode: 306]; [SCSentry captureError: err]; @@ -212,7 +212,7 @@ + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData [self.daemonMethodLock unlock]; return; } - if (![SCUtilities modernBlockIsRunning]) { + if (![SCBlockUtilities modernBlockIsRunning]) { NSLog(@"ERROR: Can't update block end date since block isn't running"); NSError* err = [SCErr errorWithCode: 307]; [SCSentry captureError: err]; @@ -269,7 +269,7 @@ + (void)checkupBlock { lastBlockIntegrityCheck = [NSDate distantPast]; } - if(![SCUtilities anyBlockIsRunning]) { + if(![SCBlockUtilities anyBlockIsRunning]) { // No block appears to be running at all in our settings. // Most likely, the user removed it trying to get around the block. Boo! // but for safety and to avoid permablocks (we no longer know when the block should end) @@ -293,7 +293,7 @@ + (void)checkupBlock { // once the checkups stop, the daemon will clear itself in a while due to inactivity [[SCDaemon sharedDaemon] stopCheckupTimer]; - } else if ([SCUtilities currentBlockIsExpired]) { + } else if ([SCBlockUtilities currentBlockIsExpired]) { NSLog(@"INFO: Checkup ran, block expired, removing block."); removeBlock(); diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m index a0f8c7e4..2ac0f4a4 100644 --- a/Daemon/SCDaemonUtilities.m +++ b/Daemon/SCDaemonUtilities.m @@ -8,7 +8,6 @@ #import "SCDaemonUtilities.h" #import #import "SCSettings.h" -#import "SCUtilities.h" #import "BlockManager.h" @implementation SCDaemonUtilities diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index e0f31320..0d25033c 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -8,7 +8,6 @@ #import "SCDaemonXPC.h" #import "SCDaemonBlockMethods.h" #import "SCXPCAuthorization.h" -#import "SCUtilities.h" @implementation SCDaemonXPC @@ -17,7 +16,7 @@ - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; if (error != nil) { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { NSLog(@"ERROR: XPC authorization failed due to error %@", error); [SCSentry captureError: error]; } @@ -53,7 +52,7 @@ - (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData NSError* error = [SCXPCAuthorization checkAuthorization: authData command: _cmd]; if (error != nil) { - if (![SCUtilities errorIsAuthCanceled: error]) { + if (![SCMiscUtilities errorIsAuthCanceled: error]) { NSLog(@"ERROR: XPC authorization failed due to error %@", error); [SCSentry captureError: error]; } diff --git a/DomainListWindowController.m b/DomainListWindowController.m index fa9aa3c3..d5e494ea 100755 --- a/DomainListWindowController.m +++ b/DomainListWindowController.m @@ -22,7 +22,6 @@ // along with this program. If not, see . #import "DomainListWindowController.h" -#import "SCUtilities.h" #import "AppController.h" @implementation DomainListWindowController @@ -130,7 +129,7 @@ - (void)tableView:(NSTableView *)aTableView return; } - NSArray* cleanedEntries = [SCUtilities cleanBlocklistEntry: newString]; + NSArray* cleanedEntries = [SCMiscUtilities cleanBlocklistEntry: newString]; for (int i = 0; i < cleanedEntries.count; i++) { NSString* entry = cleanedEntries[i]; diff --git a/SCError.strings b/SCError.strings index 9510cda9..afb41863 100644 --- a/SCError.strings +++ b/SCError.strings @@ -43,7 +43,7 @@ // 600-699 = errors generated in SCSettings "600" = "Unable to write SCSettings from a normal (non-root) account."; -// 700-799 = errors generated in SCUtilities or other utility functions +// 700-799 = errors generated in SelfControl utility functions "700" = "SelfControl couldn't clear your browser caches, because it couldn't read the user home directories."; "701" = "SelfControl couldn't clear out your settings from old version of SelfControl, because we had insufficient permissions."; "702" = "SelfControl couldn't clear out your settings from old version of SelfControl, because a block from an older version is ongoing."; diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index c0c49279..5da50a6c 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 9A31F3D484C646257DB308C5 /* Pods_SelfControl_Killer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BA749B3BC24DC33425F27DF /* Pods_SelfControl_Killer.framework */; }; CB0385E119D77051004614B6 /* PreferencesAdvancedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CB0385DD19D77051004614B6 /* PreferencesAdvancedViewController.xib */; }; CB0385E219D77051004614B6 /* PreferencesGeneralViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CB0385DF19D77051004614B6 /* PreferencesGeneralViewController.xib */; }; - CB0EEF7820FE49030024D27B /* SCUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CB0EEF7720FE49020024D27B /* SCUtilitiesTests.m */; }; + CB0EEF7820FE49030024D27B /* SCUtilityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CB0EEF7720FE49020024D27B /* SCUtilityTests.m */; }; CB114283222CCF19004B7868 /* SCSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBF3B573217BADD7006D5F52 /* SCSettings.m */; }; CB114284222CD4F0004B7868 /* SCSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBF3B573217BADD7006D5F52 /* SCSettings.m */; }; CB1465B825B027E700130D2E /* SCErr.m in Sources */ = {isa = PBXBuildFile; fileRef = CB1465B725B027E700130D2E /* SCErr.m */; }; @@ -57,7 +57,7 @@ CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = CB5DFCB62251DD1F0084CEC2 /* SCConstants.m */; }; CB62FC3024B11A4F00ADBC40 /* selfcontrold-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */; }; CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */; }; - CB62FC3F24B1327A00ADBC40 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; + CB62FC3F24B1327A00ADBC40 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBF3B573217BADD7006D5F52 /* SCSettings.m */; }; CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */; }; CB62FC4224B1329200ADBC40 /* BlockManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806116C1FDBE0059C99A /* BlockManager.m */; }; @@ -85,6 +85,20 @@ CB81A94C25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; CB81A94D25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; CB81A94E25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */; }; + CB81A9CF25B7C269006956F7 /* SCBlockUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = CB81A9CD25B7C269006956F7 /* SCBlockUtilities.h */; }; + CB81A9D025B7C269006956F7 /* SCBlockUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */; }; + CB81A9D125B7C269006956F7 /* SCBlockUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */; }; + CB81A9D225B7C269006956F7 /* SCBlockUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */; }; + CB81A9D325B7C269006956F7 /* SCBlockUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */; }; + CB81A9D425B7C269006956F7 /* SCBlockUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */; }; + CB81A9D525B7C269006956F7 /* SCBlockUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */; }; + CB81A9F225B7C5F7006956F7 /* SCBlockFileReaderWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = CB81A9F025B7C5F7006956F7 /* SCBlockFileReaderWriter.h */; }; + CB81A9F325B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; + CB81A9F425B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; + CB81A9F525B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; + CB81A9F625B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; + CB81A9F725B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; + CB81A9F825B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; CB850F3925130F5300EE2E2D /* NSString+IPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806516C237F10059C99A /* NSString+IPAddress.m */; }; CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */; }; CB90BF830F49F430006D202D /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; @@ -114,10 +128,10 @@ CBADC28125B22BC7000EE5BB /* SCSentry.m in Sources */ = {isa = PBXBuildFile; fileRef = CBADC27D25B22BC7000EE5BB /* SCSentry.m */; }; CBADC28225B22BC7000EE5BB /* SCSentry.m in Sources */ = {isa = PBXBuildFile; fileRef = CBADC27D25B22BC7000EE5BB /* SCSentry.m */; }; CBB0AE2A0FA74566006229B3 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; - CBB1731520F041F4007FCAE9 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; - CBB1731920F05C07007FCAE9 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; - CBB1731B20F05C09007FCAE9 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; - CBB1731C20F05C0A007FCAE9 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; + CBB1731520F041F4007FCAE9 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; + CBB1731920F05C07007FCAE9 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; + CBB1731B20F05C09007FCAE9 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; + CBB1731C20F05C0A007FCAE9 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; CBB3FD7A0F53834B00244132 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E901D0F397FFA006DE6E4 /* Security.framework */; }; CBB7DEEA0F53313F00ABF3EA /* DomainListWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB637220F3E296000EBD135 /* DomainListWindowController.m */; }; CBBCA14B0F54E1B300C75324 /* org.eyebeam.SelfControl.plist in Resources */ = {isa = PBXBuildFile; fileRef = CB4295C20F53EF8C008E10CA /* org.eyebeam.SelfControl.plist */; }; @@ -135,7 +149,7 @@ CBD4848A19D764440020F949 /* PreferencesGeneralViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD4848819D764440020F949 /* PreferencesGeneralViewController.m */; }; CBD4848F19D768C90020F949 /* PreferencesAdvancedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD4848D19D768C90020F949 /* PreferencesAdvancedViewController.m */; }; CBDB118C2084239D0010397E /* FirstTime.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBDB118E2084239D0010397E /* FirstTime.xib */; }; - CBDF919A225C5A9700358B95 /* SCUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCUtilities.m */; }; + CBDF919A225C5A9700358B95 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; CBDFFF4724A0450200622CEE /* org.eyebeam.selfcontrold in Copy Daemon Launch Service */ = {isa = PBXBuildFile; fileRef = CB74D11D2480E506002B2079 /* org.eyebeam.selfcontrold */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CBDFFF4924A0711800622CEE /* org.eyebeam.selfcontrold.plist in Resources */ = {isa = PBXBuildFile; fileRef = CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */; }; CBE2BB5619D10CCC0077124F /* SCKillerHelper in Copy Executable Helper Tools */ = {isa = PBXBuildFile; fileRef = CB9C811B19CFBA8500CDCAE1 /* SCKillerHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -224,7 +238,7 @@ B1812886BF1D4F103F3E8D1D /* Pods-org.eyebeam.selfcontrold.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-org.eyebeam.selfcontrold.release.xcconfig"; path = "Pods/Target Support Files/Pods-org.eyebeam.selfcontrold/Pods-org.eyebeam.selfcontrold.release.xcconfig"; sourceTree = ""; }; CB0EEF5D20FD8CE00024D27B /* SelfControlTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SelfControlTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CB0EEF6120FD8CE00024D27B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - CB0EEF7720FE49020024D27B /* SCUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SCUtilitiesTests.m; sourceTree = ""; }; + CB0EEF7720FE49020024D27B /* SCUtilityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SCUtilityTests.m; sourceTree = ""; }; CB1465B625B027E700130D2E /* SCErr.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SCErr.h; path = Common/SCErr.h; sourceTree = SOURCE_ROOT; }; CB1465B725B027E700130D2E /* SCErr.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SCErr.m; path = Common/SCErr.m; sourceTree = SOURCE_ROOT; }; CB1465C325B0285300130D2E /* SCError.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = SCError.strings; sourceTree = ""; }; @@ -271,6 +285,11 @@ CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = org.eyebeam.selfcontrold.plist; sourceTree = ""; }; CB81A94625B7B5B5006956F7 /* SCMigrationUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCMigrationUtilities.h; sourceTree = ""; }; CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCMigrationUtilities.m; sourceTree = ""; }; + CB81A9CD25B7C269006956F7 /* SCBlockUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCBlockUtilities.h; sourceTree = ""; }; + CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCBlockUtilities.m; sourceTree = ""; }; + CB81A9E225B7C2A8006956F7 /* SCUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCUtility.h; sourceTree = ""; }; + CB81A9F025B7C5F7006956F7 /* SCBlockFileReaderWriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCBlockFileReaderWriter.h; sourceTree = ""; }; + CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCBlockFileReaderWriter.m; sourceTree = ""; }; CB850F3A2513185300EE2E2D /* SCDaemonUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonUtilities.h; sourceTree = ""; }; CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonUtilities.m; sourceTree = ""; }; CB90BF810F49F430006D202D /* HostImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostImporter.h; sourceTree = ""; }; @@ -297,8 +316,8 @@ CBADC27D25B22BC7000EE5BB /* SCSentry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCSentry.m; sourceTree = ""; }; CBB0AE280FA74566006229B3 /* HostFileBlocker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostFileBlocker.h; sourceTree = ""; }; CBB0AE290FA74566006229B3 /* HostFileBlocker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HostFileBlocker.m; sourceTree = ""; }; - CBB1731220F041F4007FCAE9 /* SCUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCUtilities.h; sourceTree = ""; }; - CBB1731320F041F4007FCAE9 /* SCUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCUtilities.m; sourceTree = ""; }; + CBB1731220F041F4007FCAE9 /* SCMiscUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCMiscUtilities.h; sourceTree = ""; }; + CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCMiscUtilities.m; sourceTree = ""; }; CBB60BE31F12F19E00DCB597 /* distribution-build.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = "distribution-build.rb"; sourceTree = ""; }; CBB637210F3E296000EBD135 /* DomainListWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DomainListWindowController.h; sourceTree = ""; }; CBB637220F3E296000EBD135 /* DomainListWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DomainListWindowController.m; sourceTree = ""; }; @@ -590,7 +609,7 @@ CB0EEF5E20FD8CE00024D27B /* SelfControlTests */ = { isa = PBXGroup; children = ( - CB0EEF7720FE49020024D27B /* SCUtilitiesTests.m */, + CB0EEF7720FE49020024D27B /* SCUtilityTests.m */, CB0EEF6120FD8CE00024D27B /* Info.plist */, ); path = SelfControlTests; @@ -599,12 +618,11 @@ CB1CA64C25ABA5990084A551 /* Common */ = { isa = PBXGroup; children = ( - CB81A94625B7B5B5006956F7 /* SCMigrationUtilities.h */, - CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */, - CBB1731220F041F4007FCAE9 /* SCUtilities.h */, - CBB1731320F041F4007FCAE9 /* SCUtilities.m */, + CB81A9E325B7C2B9006956F7 /* Utility */, CBADC27C25B22BC7000EE5BB /* SCSentry.h */, CBADC27D25B22BC7000EE5BB /* SCSentry.m */, + CB81A9F025B7C5F7006956F7 /* SCBlockFileReaderWriter.h */, + CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */, CB1465B625B027E700130D2E /* SCErr.h */, CB1465B725B027E700130D2E /* SCErr.m */, CBF3B572217BADD7006D5F52 /* SCSettings.h */, @@ -689,6 +707,20 @@ path = Daemon; sourceTree = ""; }; + CB81A9E325B7C2B9006956F7 /* Utility */ = { + isa = PBXGroup; + children = ( + CB81A9E225B7C2A8006956F7 /* SCUtility.h */, + CB81A94625B7B5B5006956F7 /* SCMigrationUtilities.h */, + CB81A94725B7B5B5006956F7 /* SCMigrationUtilities.m */, + CB81A9CD25B7C269006956F7 /* SCBlockUtilities.h */, + CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */, + CBB1731220F041F4007FCAE9 /* SCMiscUtilities.h */, + CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */, + ); + path = Utility; + sourceTree = ""; + }; CB9C80F819CFB79700CDCAE1 /* SelfControl Killer */ = { isa = PBXGroup; children = ( @@ -773,6 +805,8 @@ buildActionMask = 2147483647; files = ( CB81A94825B7B5B5006956F7 /* SCMigrationUtilities.h in Headers */, + CB81A9CF25B7C269006956F7 /* SCBlockUtilities.h in Headers */, + CB81A9F225B7C5F7006956F7 /* SCBlockFileReaderWriter.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1250,8 +1284,9 @@ CB1CA66525ABA6240084A551 /* SCXPCAuthorization.m in Sources */, CB529BBF0F32B7ED00564FB8 /* AppController.m in Sources */, CB81A94925B7B5B5006956F7 /* SCMigrationUtilities.m in Sources */, + CB81A9F325B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CB1465B825B027E700130D2E /* SCErr.m in Sources */, - CBB1731920F05C07007FCAE9 /* SCUtilities.m in Sources */, + CBB1731920F05C07007FCAE9 /* SCMiscUtilities.m in Sources */, CB58948025B3FC6D00E9A5C0 /* HostFileBlocker.m in Sources */, F5B8CBEE19EE21C30026F3A5 /* SCTimeIntervalFormatter.m in Sources */, CBD4848A19D764440020F949 /* PreferencesGeneralViewController.m in Sources */, @@ -1262,6 +1297,7 @@ CBE4401B0F4BE0670062A1FE /* ThunderbirdPreferenceParser.m in Sources */, CBE5C40B0F4D4531003DB900 /* ButtonWithPopupMenu.m in Sources */, CBD4848F19D768C90020F949 /* PreferencesAdvancedViewController.m in Sources */, + CB81A9D025B7C269006956F7 /* SCBlockUtilities.m in Sources */, CBB7DEEA0F53313F00ABF3EA /* DomainListWindowController.m in Sources */, CBF3B574217BADD7006D5F52 /* SCSettings.m in Sources */, CB25806616C237F10059C99A /* NSString+IPAddress.m in Sources */, @@ -1272,10 +1308,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CBDF919A225C5A9700358B95 /* SCUtilities.m in Sources */, + CBDF919A225C5A9700358B95 /* SCMiscUtilities.m in Sources */, CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */, + CB81A9D425B7C269006956F7 /* SCBlockUtilities.m in Sources */, + CB81A9F725B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CB114284222CD4F0004B7868 /* SCSettings.m in Sources */, - CB0EEF7820FE49030024D27B /* SCUtilitiesTests.m in Sources */, + CB0EEF7820FE49030024D27B /* SCUtilityTests.m in Sources */, CB81A94D25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1289,13 +1327,15 @@ CB62FC4624B132A300ADBC40 /* HostImporter.m in Sources */, CB62FC4724B132A600ADBC40 /* AllowlistScraper.m in Sources */, CB62FC4524B1329F00ADBC40 /* ThunderbirdPreferenceParser.m in Sources */, + CB81A9F825B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CB74D1202480E566002B2079 /* SCDaemon.m in Sources */, CBADC28225B22BC7000EE5BB /* SCSentry.m in Sources */, CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */, - CB62FC3F24B1327A00ADBC40 /* SCUtilities.m in Sources */, + CB62FC3F24B1327A00ADBC40 /* SCMiscUtilities.m in Sources */, CB1465BC25B027E700130D2E /* SCErr.m in Sources */, CB62FC4924B1330700ADBC40 /* LaunchctlHelper.m in Sources */, CB62FC4824B132B300ADBC40 /* SCConstants.m in Sources */, + CB81A9D525B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */, CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */, CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, @@ -1313,13 +1353,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CB81A9D225B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB9C80FF19CFB79700CDCAE1 /* AppDelegate.m in Sources */, CB9C80FC19CFB79700CDCAE1 /* main.m in Sources */, CB5DFCBA2251DD1F0084CEC2 /* SCConstants.m in Sources */, CB32D2AC21902CF800B8CD68 /* SCSettings.m in Sources */, + CB81A9F525B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CB58948725B3FC6F00E9A5C0 /* HostFileBlocker.m in Sources */, CB1465BA25B027E700130D2E /* SCErr.m in Sources */, - CBB1731C20F05C0A007FCAE9 /* SCUtilities.m in Sources */, + CBB1731C20F05C0A007FCAE9 /* SCMiscUtilities.m in Sources */, CBADC28025B22BC7000EE5BB /* SCSentry.m in Sources */, CB81A94B25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, ); @@ -1340,11 +1382,13 @@ CB81A94C25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CB1CA64F25ABA5BB0084A551 /* SCXPCClient.m in Sources */, CB114283222CCF19004B7868 /* SCSettings.m in Sources */, + CB81A9D325B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB9C812319CFBB4400CDCAE1 /* LaunchctlHelper.m in Sources */, CB1CA64D25ABA5BB0084A551 /* SCXPCAuthorization.m in Sources */, CB9C812219CFBB3800CDCAE1 /* PacketFilter.m in Sources */, CB1465BB25B027E700130D2E /* SCErr.m in Sources */, - CBB1731B20F05C09007FCAE9 /* SCUtilities.m in Sources */, + CBB1731B20F05C09007FCAE9 /* SCMiscUtilities.m in Sources */, + CB81A9F625B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1354,9 +1398,11 @@ files = ( CBADC27F25B22BC7000EE5BB /* SCSentry.m in Sources */, CB1CA64E25ABA5BB0084A551 /* SCXPCAuthorization.m in Sources */, + CB81A9D125B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB20C5D8245699D700B9D749 /* version-header.h in Sources */, CBA2AFD90F39EC46005AFEBE /* cli-main.m in Sources */, CB5DFCB82251DD1F0084CEC2 /* SCConstants.m in Sources */, + CB81A9F425B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CBCA91121960D87300AFD20C /* PacketFilter.m in Sources */, CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */, CBC2F8580F4672FE00CF2A42 /* LaunchctlHelper.m in Sources */, @@ -1368,7 +1414,7 @@ CB25806216C1FDBE0059C99A /* BlockManager.m in Sources */, CB25806716C237F10059C99A /* NSString+IPAddress.m in Sources */, CB1465B925B027E700130D2E /* SCErr.m in Sources */, - CBB1731520F041F4007FCAE9 /* SCUtilities.m in Sources */, + CBB1731520F041F4007FCAE9 /* SCMiscUtilities.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SelfControlTests/SCUtilitiesTests.m b/SelfControlTests/SCUtilityTests.m similarity index 74% rename from SelfControlTests/SCUtilitiesTests.m rename to SelfControlTests/SCUtilityTests.m index ee60a491..a92caa7b 100644 --- a/SelfControlTests/SCUtilitiesTests.m +++ b/SelfControlTests/SCUtilityTests.m @@ -6,12 +6,12 @@ // #import -#import "SCUtilities.h" +#import "SCUtility.h" #import "SCSentry.h" #import "SCErr.h" #import "SCSettings.h" -@interface SCUtilitiesTests : XCTestCase +@interface SCUtilityTests : XCTestCase @end @@ -26,7 +26,7 @@ @interface SCUtilitiesTests : XCTestCase NSDictionary* negativeBlockDurationLegacyDict; // block duration is negative NSDictionary* veryLongBlockLegacyDict; // year-long block, one day in -@implementation SCUtilitiesTests +@implementation SCUtilityTests - (NSUserDefaults*)testDefaults { return [[NSUserDefaults alloc] initWithSuiteName: @"BlockDateUtilitiesTests"]; @@ -82,54 +82,54 @@ - (void)tearDown { - (void) testCleanBlocklistEntries { // ignores weird invalid entries - XCTAssert([SCUtilities cleanBlocklistEntry: nil].count == 0); - XCTAssert([SCUtilities cleanBlocklistEntry: @""].count == 0); - XCTAssert([SCUtilities cleanBlocklistEntry: @" "].count == 0); - XCTAssert([SCUtilities cleanBlocklistEntry: @" \n\n \n***!@#$%^*()+=<>,/?| "].count == 0); - XCTAssert([SCUtilities cleanBlocklistEntry: @"://}**"].count == 0); + XCTAssert([SCMiscUtilities cleanBlocklistEntry: nil].count == 0); + XCTAssert([SCMiscUtilities cleanBlocklistEntry: @""].count == 0); + XCTAssert([SCMiscUtilities cleanBlocklistEntry: @" "].count == 0); + XCTAssert([SCMiscUtilities cleanBlocklistEntry: @" \n\n \n***!@#$%^*()+=<>,/?| "].count == 0); + XCTAssert([SCMiscUtilities cleanBlocklistEntry: @"://}**"].count == 0); // can take a plain hostname - NSArray* cleaned = [SCUtilities cleanBlocklistEntry: @"selfcontrolapp.com"]; + NSArray* cleaned = [SCMiscUtilities cleanBlocklistEntry: @"selfcontrolapp.com"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"selfcontrolapp.com"]); // and lowercase it - cleaned = [SCUtilities cleanBlocklistEntry: @"selFconTROLapp.com"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"selFconTROLapp.com"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"selfcontrolapp.com"]); // with subdomains - cleaned = [SCUtilities cleanBlocklistEntry: @"www.selFconTROLapp.com"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"www.selFconTROLapp.com"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"www.selfcontrolapp.com"]); // with http scheme - cleaned = [SCUtilities cleanBlocklistEntry: @"http://www.selFconTROLapp.com"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"http://www.selFconTROLapp.com"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"www.selfcontrolapp.com"]); // with https scheme - cleaned = [SCUtilities cleanBlocklistEntry: @"https://www.selFconTROLapp.com"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"https://www.selFconTROLapp.com"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"www.selfcontrolapp.com"]); // with ftp scheme - cleaned = [SCUtilities cleanBlocklistEntry: @"ftp://www.selFconTROLapp.com"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"ftp://www.selFconTROLapp.com"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"www.selfcontrolapp.com"]); // with port - cleaned = [SCUtilities cleanBlocklistEntry: @"https://www.selFconTROLapp.com:73"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"https://www.selFconTROLapp.com:73"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"www.selfcontrolapp.com:73"]); // strips username/password - cleaned = [SCUtilities cleanBlocklistEntry: @"http://charlie:mypass@cnn.com:54"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"http://charlie:mypass@cnn.com:54"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"cnn.com:54"]); // strips path etc - cleaned = [SCUtilities cleanBlocklistEntry: @"http://mysite.com/my/path/is/very/long.php?querystring=ydfjkl&otherquerystring=%40%80%20#cool"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"http://mysite.com/my/path/is/very/long.php?querystring=ydfjkl&otherquerystring=%40%80%20#cool"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"mysite.com"]); // CIDR IP ranges - cleaned = [SCUtilities cleanBlocklistEntry: @"127.0.0.1/20"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"127.0.0.1/20"]; XCTAssert(cleaned.count == 1 && [[cleaned firstObject] isEqualToString: @"127.0.0.1/20"]); // can split entries by newlines - cleaned = [SCUtilities cleanBlocklistEntry: @"http://charlie:mypass@cnn.com:54\nhttps://selfcontrolAPP.com\n192.168.1.1/24\ntest.com\n{}*&\nhttps://reader.google.com/mypath/is/great.php"]; + cleaned = [SCMiscUtilities cleanBlocklistEntry: @"http://charlie:mypass@cnn.com:54\nhttps://selfcontrolAPP.com\n192.168.1.1/24\ntest.com\n{}*&\nhttps://reader.google.com/mypath/is/great.php"]; XCTAssert(cleaned.count == 5); XCTAssert([cleaned[0] isEqualToString: @"cnn.com:54"]); XCTAssert([cleaned[1] isEqualToString: @"selfcontrolapp.com"]); @@ -141,8 +141,8 @@ - (void) testCleanBlocklistEntries { - (void) testModernBlockDetection { SCSettings* settings = [SCSettings sharedSettings]; - XCTAssert(![SCUtilities modernBlockIsRunning]); - XCTAssert([SCUtilities currentBlockIsExpired]); + XCTAssert(![SCBlockUtilities modernBlockIsRunning]); + XCTAssert([SCBlockUtilities currentBlockIsExpired]); // test a block that should have expired 5 minutes ago [settings setValue: @YES forKey: @"BlockIsRunning"]; @@ -150,18 +150,18 @@ - (void) testModernBlockDetection { [settings setValue: @NO forKey: @"ActiveBlockAsWhitelist"]; [settings setValue: [NSDate dateWithTimeIntervalSinceNow: -300] forKey: @"BlockEndDate"]; - XCTAssert([SCUtilities modernBlockIsRunning]); - XCTAssert([SCUtilities currentBlockIsExpired]); + XCTAssert([SCBlockUtilities modernBlockIsRunning]); + XCTAssert([SCBlockUtilities currentBlockIsExpired]); // test block that should still be running [settings setValue: [NSDate dateWithTimeIntervalSinceNow: 300] forKey: @"BlockEndDate"]; - XCTAssert([SCUtilities modernBlockIsRunning]); - XCTAssert(![SCUtilities currentBlockIsExpired]); + XCTAssert([SCBlockUtilities modernBlockIsRunning]); + XCTAssert(![SCBlockUtilities currentBlockIsExpired]); // test removing a block - [SCUtilities removeBlockFromSettings]; - XCTAssert(![SCUtilities modernBlockIsRunning]); - XCTAssert([SCUtilities currentBlockIsExpired]); + [SCBlockUtilities removeBlockFromSettings]; + XCTAssert(![SCBlockUtilities modernBlockIsRunning]); + XCTAssert([SCBlockUtilities currentBlockIsExpired]); } - (void) testLegacyBlockDetection { diff --git a/SelfControl_Prefix.pch b/SelfControl_Prefix.pch index b286b9d5..d57096bf 100755 --- a/SelfControl_Prefix.pch +++ b/SelfControl_Prefix.pch @@ -8,8 +8,10 @@ #import #import "version-header.h" +#import "SCUtility.h" #import "SCErr.h" #import "SCConstants.h" #import "SCSentry.h" +#import "SCSettings.h" #endif diff --git a/TimerWindowController.m b/TimerWindowController.m index 61959f1a..dd3bf63b 100755 --- a/TimerWindowController.m +++ b/TimerWindowController.m @@ -23,7 +23,6 @@ #import "TimerWindowController.h" -#import "SCUtilities.h" @interface TimerWindowController () @@ -76,7 +75,7 @@ - (void)awakeFromNib { addToBlockButton_.hidden = NO; extendBlockButton_.hidden = NO; - if ([SCUtilities modernBlockIsRunning]) { + if ([SCBlockUtilities modernBlockIsRunning]) { blockEndingDate_ = [settings_ valueForKey: @"BlockEndDate"]; } else { // legacy block! @@ -85,7 +84,7 @@ - (void)awakeFromNib { // if it's a legacy block, we will disable some features // since it's too difficult to get these working across versions. // the user will just have to wait until their next block to do these things! - if ([SCUtilities legacyBlockIsRunning]) { + if ([SCBlockUtilities legacyBlockIsRunning]) { addToBlockButton_.enabled = YES; extendBlockButton_.enabled = YES; } @@ -215,7 +214,7 @@ - (void)updateTimerDisplay:(NSTimer*)timer { // make sure add to list is disabled if it's an allowlist block // don't worry about it for a legacy block! the buttons are disabled anyway so it doesn't matter - if ([SCUtilities modernBlockIsRunning]) { + if ([SCBlockUtilities modernBlockIsRunning]) { addToBlockButton_.hidden = [settings_ boolForKey: @"ActiveBlockAsWhitelist"]; } } @@ -281,7 +280,7 @@ - (IBAction) performExtendBlock:(id)sender { } - (void) blockEndDateUpdated { - if ([SCUtilities modernBlockIsRunning]) { + if ([SCBlockUtilities modernBlockIsRunning]) { blockEndingDate_ = [settings_ valueForKey: @"BlockEndDate"]; } else { diff --git a/cli-main.m b/cli-main.m index 80e68258..becf1762 100755 --- a/cli-main.m +++ b/cli-main.m @@ -22,9 +22,9 @@ #import "PacketFilter.h" #import "HelperCommon.h" -#import "SCUtilities.h" #import "SCSettings.h" #import "SCXPCClient.h" +#import "SCBlockFileReaderWriter.h" // The main method which deals which most of the logic flow and execution of // the CLI tool. @@ -47,7 +47,7 @@ int main(int argc, char* argv[]) { // if we're running as root/sudo and we have a controlling UID, use defaults for the controlling user (legacy behavior) // otherwise, just use the current user's defaults (modern behavior) if (geteuid() == 0 && controllingUID > 0) { - defaultsDict = [SCUtilities defaultsDictForUser: controllingUID]; + defaultsDict = [SCMiscUtilities defaultsDictForUser: controllingUID]; } else { defaultsDict = [NSUserDefaults standardUserDefaults].dictionaryRepresentation; } @@ -55,7 +55,7 @@ int main(int argc, char* argv[]) { if([modeString isEqual: @"--install"]) { [SCSentry addBreadcrumb: @"CLI method --install called" category: @"cli"]; - if ([SCUtilities anyBlockIsRunning]) { + if ([SCBlockUtilities anyBlockIsRunning]) { NSLog(@"ERROR: Block is already running"); exit(EX_CONFIG); } @@ -81,7 +81,7 @@ int main(int argc, char* argv[]) { exit(EX_IOERR); } else { blockEndDate = blockEndDateArg; - NSDictionary* readProperties = [SCUtilities readBlocklistFromFile: [NSURL fileURLWithPath: pathToBlocklistFile]]; + NSDictionary* readProperties = [SCBlockFileReaderWriter readBlocklistFromFile: [NSURL fileURLWithPath: pathToBlocklistFile]]; if (readProperties == nil) { NSLog(@"ERROR: Block could not be read from file %@", pathToBlocklistFile); @@ -182,7 +182,7 @@ int main(int argc, char* argv[]) { NSLog(@"%@", [settings dictionaryRepresentation]); } else if ([modeString isEqualToString: @"--is-running"]) { [SCSentry addBreadcrumb: @"CLI method --is-running called" category: @"cli"]; - BOOL blockIsRunning = [SCUtilities anyBlockIsRunning]; + BOOL blockIsRunning = [SCBlockUtilities anyBlockIsRunning]; NSLog(@"%@", blockIsRunning ? @"YES" : @"NO"); } else if ([modeString isEqualToString: @"--version"]) { [SCSentry addBreadcrumb: @"CLI method --version called" category: @"cli"]; From f025ead55d3619319e1c8da3ef3107bf6329e1dd Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 19:12:11 -0800 Subject: [PATCH 57/72] Move HelperCommon to new SCHelperToolUtilities class --- AppController.h | 8 +- Common/HelperCommon.h | 8 +- Common/HelperCommon.m | 56 +------- Common/SCBlockFileReaderWriter.h | 9 +- Common/Utility/SCHelperToolUtilities.h | 48 +++++++ Common/Utility/SCHelperToolUtilities.m | 185 +++++++++++++++++++++++++ Common/Utility/SCMiscUtilities.h | 2 - Common/Utility/SCMiscUtilities.m | 40 +----- Common/Utility/SCUtility.h | 1 + Daemon/DaemonMain.m | 1 + Daemon/SCDaemon.h | 6 + Daemon/SCDaemon.m | 3 +- Daemon/SCDaemonBlockMethods.h | 8 ++ Daemon/SCDaemonBlockMethods.m | 28 ++-- Daemon/SCDaemonProtocol.h | 6 +- Daemon/SCDaemonUtilities.h | 18 --- Daemon/SCDaemonUtilities.m | 39 ------ Daemon/SCDaemonXPC.h | 2 + Daemon/SCDaemonXPC.m | 8 -- NSString+IPAddress.h | 1 + SCKillerHelper/main.m | 5 +- SelfControl.xcodeproj/project.pbxproj | 20 ++- cli-main.m | 3 +- 23 files changed, 311 insertions(+), 194 deletions(-) create mode 100644 Common/Utility/SCHelperToolUtilities.h create mode 100644 Common/Utility/SCHelperToolUtilities.m delete mode 100644 Daemon/SCDaemonUtilities.h delete mode 100644 Daemon/SCDaemonUtilities.m diff --git a/AppController.h b/AppController.h index 7936d9b0..24066459 100755 --- a/AppController.h +++ b/AppController.h @@ -73,16 +73,13 @@ // user interface. Called very often by several parts of the program. - (void)refreshUserInterface; -- (void)handleConfigurationChangedNotification; - // Called when the "Edit blocklist" button is clicked or the menu item is // selected. Allocates a new DomainListWindowController if necessary and opens // the domain blocklist window. Spawns an alert box if a block is in progress. - (IBAction)showDomainList:(id)sender; -// Returns YES if, according to a flag set in the user defaults system, the -// SelfControl launchd daemon (and therefore the block) is loaded. Returns NO -// if it is not. +// Returns YES if, according to settings or the hostfile, the +// SelfControl block is running Returns NO if it is not. @property (nonatomic, readonly) BOOL blockIsRunning; // Allocates a new TimerWindowController if necessary and opens the timer window. @@ -135,6 +132,7 @@ // Changed property to manual accessor for pre-Leopard compatibility @property (nonatomic, readonly, strong) id initialWindow; +// opens the SelfControl FAQ in the default browser - (IBAction)openFAQ:(id)sender; @end diff --git a/Common/HelperCommon.h b/Common/HelperCommon.h index b13b553e..fd1b419f 100644 --- a/Common/HelperCommon.h +++ b/Common/HelperCommon.h @@ -33,14 +33,10 @@ // Reads the domain block list from the settings for SelfControl, and adds deny // rules for all of the IPs (or the A DNS record IPS for doamin names) to the // ipfw firewall. -void addRulesToFirewall(void); +void installBlockRulesFromSettings(void); // Removes from ipfw all rules that were created by SelfControl. -void removeRulesFromFirewall(void); - -// Returns an autoreleased NSSet containing all IP adresses for evaluated -// "common subdomains" for the specified hostname -NSSet* getEvaluatedHostNamesFromCommonSubdomains(NSString* hostName, int port); +void uninstallBlockRules(void); // Checks the settings system to see whether the user wants their web browser // caches cleared, and deletes the specific cache folders for a few common diff --git a/Common/HelperCommon.m b/Common/HelperCommon.m index 75bccd42..55cbb87e 100644 --- a/Common/HelperCommon.m +++ b/Common/HelperCommon.m @@ -12,7 +12,7 @@ #import "SCSettings.h" #import -void addRulesToFirewall() { +void installBlockRulesFromSettings() { SCSettings* settings = [SCSettings sharedSettings]; BOOL shouldEvaluateCommonSubdomains = [settings boolForKey: @"EvaluateCommonSubdomains"]; BOOL allowLocalNetworks = [settings boolForKey: @"AllowLocalNetworks"]; @@ -31,7 +31,7 @@ void addRulesToFirewall() { } -void removeRulesFromFirewall() { +void uninstallBlockRules() { // options don't really matter because we're only using it to clear BlockManager* blockManager = [[BlockManager alloc] init]; [blockManager clearBlock]; @@ -57,61 +57,19 @@ void removeRulesFromFirewall() { } } -NSSet* getEvaluatedHostNamesFromCommonSubdomains(NSString* hostName, int port) { - NSMutableSet* evaluatedAddresses = [NSMutableSet set]; - - // If the domain ends in facebook.com... Special case for Facebook because - // users will often forget to block some of its many mirror subdomains that resolve - // to different IPs, i.e. hs.facebook.com. Thanks to Danielle for raising this issue. - if([hostName rangeOfString: @"facebook.com"].location == ([hostName length] - 12)) { - [evaluatedAddresses addObject: @"69.63.176.0/20"]; - } - - // Block the domain with no subdomains, if www.domain is blocked - else if([hostName rangeOfString: @"www."].location == 0) { - NSHost* modifiedHost = [NSHost hostWithName: [hostName substringFromIndex: 4]]; - - if(modifiedHost) { - NSArray* addresses = [modifiedHost addresses]; - - for(int j = 0; j < [addresses count]; j++) { - if(port != -1) - [evaluatedAddresses addObject: [NSString stringWithFormat: @"%@:%d", addresses[j], port]]; - else [evaluatedAddresses addObject: addresses[j]]; - } - } - } - // Or block www.domain otherwise - else { - NSHost* modifiedHost = [NSHost hostWithName: [@"www." stringByAppendingString: hostName]]; - - if(modifiedHost) { - NSArray* addresses = [modifiedHost addresses]; - - for(int j = 0; j < [addresses count]; j++) { - if(port != -1) - [evaluatedAddresses addObject: [NSString stringWithFormat: @"%@:%d", addresses[j], port]]; - else [evaluatedAddresses addObject: addresses[j]]; - } - } - } - - return evaluatedAddresses; -} - void clearCachesIfRequested() { SCSettings* settings = [SCSettings sharedSettings]; if(![settings boolForKey: @"ClearCaches"]) { return; } - NSError* err = [SCMiscUtilities clearBrowserCaches]; + NSError* err = [SCHelperToolUtilities clearBrowserCaches]; if (err) { NSLog(@"WARNING: Error clearing browser caches: %@", err); [SCSentry captureError: err]; } - clearOSDNSCache(); + [SCHelperToolUtilities clearOSDNSCache]; } void clearOSDNSCache() { @@ -141,17 +99,17 @@ void removeBlock() { SCSettings* settings = [SCSettings sharedSettings]; [SCBlockUtilities removeBlockFromSettings]; - removeRulesFromFirewall(); + [SCHelperToolUtilities uninstallBlockRules]; // always synchronize settings ASAP after removing a block to let everybody else know [settings synchronizeSettings]; // let the main app know things have changed so it can update the UI! - sendConfigurationChangedNotification(); + [SCHelperToolUtilities sendConfigurationChangedNotification]; NSLog(@"INFO: Block cleared."); - clearCachesIfRequested(); + [SCHelperToolUtilities clearCachesIfRequested]; } void sendConfigurationChangedNotification() { diff --git a/Common/SCBlockFileReaderWriter.h b/Common/SCBlockFileReaderWriter.h index c10ce16a..55c06626 100644 --- a/Common/SCBlockFileReaderWriter.h +++ b/Common/SCBlockFileReaderWriter.h @@ -9,10 +9,17 @@ NS_ASSUME_NONNULL_BEGIN +// read and write saved .selfcontrol block files @interface SCBlockFileReaderWriter : NSObject -// read and write saved block files +// Writes out a saved .selfcontrol blocklist file to the file system +// containing the block info (blocklist + whitelist setting) defined +// in blockInfo. + (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; + +// reads in a saved .selfcontrol blocklist file and returns +// an NSDictionary with the block settings contained +// (properties are Blocklist and BlockAsWhitelist) + (NSDictionary*)readBlocklistFromFile:(NSURL*)fileURL; @end diff --git a/Common/Utility/SCHelperToolUtilities.h b/Common/Utility/SCHelperToolUtilities.h new file mode 100644 index 00000000..f51da13f --- /dev/null +++ b/Common/Utility/SCHelperToolUtilities.h @@ -0,0 +1,48 @@ +// +// SCHelperToolUtilities.h +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// Utility methods athat are only used by the helper tools +// (i.e. selfcontrold, selfcontrol-cli, and SCKillerHelper) +// note that this is NOT included in SCUtility.h currently! +@interface SCHelperToolUtilities : NSObject + +// Reads the domain block list from the settings for SelfControl, and adds deny +// rules for all of the IPs (or the A DNS record IPS for doamin names) to the +// ipfw firewall. ++ (void)installBlockRulesFromSettings; + +// Removes from ipfw all rules that were created by SelfControl. ++ (void)uninstallBlockRules; + +// calls SMJobRemove to unload the daemon from launchd +// (which also kills the running process, synchronously) ++ (void)unloadDaemonJob; + +// Checks the settings system to see whether the user wants their web browser +// caches cleared, and deletes the specific cache folders for a few common +// web browsers if it is required. ++ (void)clearCachesIfRequested; + +// Clear only the caches for browsers ++ (NSError*)clearBrowserCaches; + +// Clear only the OS-level DNS cache ++ (void)clearOSDNSCache; + +// Removes block via settings, host file rules and ipfw rules, +// deleting user caches if requested, and migrating legacy settings. ++ (void)removeBlock; + ++ (void)sendConfigurationChangedNotification; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Common/Utility/SCHelperToolUtilities.m b/Common/Utility/SCHelperToolUtilities.m new file mode 100644 index 00000000..7751e072 --- /dev/null +++ b/Common/Utility/SCHelperToolUtilities.m @@ -0,0 +1,185 @@ +// +// SCHelperToolUtilities.m +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#import "SCHelperToolUtilities.h" +#import "BlockManager.h" +#import + +@implementation SCHelperToolUtilities + ++ (void)installBlockRulesFromSettings { + SCSettings* settings = [SCSettings sharedSettings]; + BOOL shouldEvaluateCommonSubdomains = [settings boolForKey: @"EvaluateCommonSubdomains"]; + BOOL allowLocalNetworks = [settings boolForKey: @"AllowLocalNetworks"]; + BOOL includeLinkedDomains = [settings boolForKey: @"IncludeLinkedDomains"]; + + // get value for ActiveBlockAsWhitelist + BOOL blockAsAllowlist = [settings boolForKey: @"ActiveBlockAsWhitelist"]; + + BlockManager* blockManager = [[BlockManager alloc] initAsAllowlist: blockAsAllowlist allowLocal: allowLocalNetworks includeCommonSubdomains: shouldEvaluateCommonSubdomains includeLinkedDomains: includeLinkedDomains]; + + NSLog(@"About to run BlockManager commands"); + + [blockManager prepareToAddBlock]; + [blockManager addBlockEntries: [settings valueForKey: @"ActiveBlocklist"]]; + [blockManager finalizeBlock]; + +} + ++ (void)uninstallBlockRules { + // options don't really matter because we're only using it to clear + BlockManager* blockManager = [[BlockManager alloc] init]; + [blockManager clearBlock]; + + // We'll play the sound now rather than earlier, because + // it is important that the UI get updated (by the posted + // notification) before we sleep to play the sound. Otherwise, + // the app seems unresponsive and slow. + SCSettings* settings = [SCSettings sharedSettings]; + if([settings boolForKey: @"BlockSoundShouldPlay"]) { + // Map the tags used in interface builder to the sound + NSArray* systemSoundNames = SCConstants.systemSoundNames; + NSSound* alertSound = [NSSound soundNamed: systemSoundNames[[[settings valueForKey: @"BlockSound"] intValue]]]; + if(!alertSound) + NSLog(@"WARNING: Alert sound not found."); + else { + [alertSound play]; + // Sleeping a second is a messy way of doing this, but otherwise the + // sound is killed along with this process when it is unloaded in just + // a few lines. + sleep(1); + } + } +} + ++ (void)unloadDaemonJob { + NSLog(@"Unloading SelfControl daemon..."); + [SCSentry addBreadcrumb: @"Daemon about to unload" category: @"daemon"]; + SCSettings* settings = [SCSettings sharedSettings]; + + // we're about to unload the launchd job + // this will kill this process, so we have to make sure + // all settings are synced before we unload + NSError* syncErr; + [settings syncSettingsAndWait: 5.0 error: &syncErr]; + if (syncErr != nil) { + NSLog(@"WARNING: Sync failed or timed out with error %@ before unloading daemon job", syncErr); + [SCSentry captureError: syncErr]; + } + + // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. + CFErrorRef cfError; + // this should block until the process is dead, so we should never get to the other side if it's successful + SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, YES, &cfError); + if (cfError) { + NSLog(@"Failed to remove selfcontrold daemon with error %@", cfError); + } +} + ++ (void)clearCachesIfRequested { + SCSettings* settings = [SCSettings sharedSettings]; + if(![settings boolForKey: @"ClearCaches"]) { + return; + } + + NSError* err = [SCHelperToolUtilities clearBrowserCaches]; + if (err) { + NSLog(@"WARNING: Error clearing browser caches: %@", err); + [SCSentry captureError: err]; + } + + [SCHelperToolUtilities clearOSDNSCache]; +} + ++ (NSError*)clearBrowserCaches { + NSFileManager* fileManager = [NSFileManager defaultManager]; + + NSError* homeDirErr = nil; + NSArray* homeDirectoryURLs = [SCMiscUtilities allUserHomeDirectoryURLs: &homeDirErr]; + if (homeDirectoryURLs == nil) return homeDirErr; + + NSArray* cacheDirPathComponents = @[ + // chrome + @"/Library/Caches/Google/Chrome/Default", + @"/Library/Caches/Google/Chrome/com.google.Chrome", + + // firefox + @"/Library/Caches/Firefox/Profiles", + + // safari + @"/Library/Caches/com.apple.Safari", + @"/Library/Containers/com.apple.Safari/Data/Library/Caches" // this one seems to fail due to permissions issues, but not sure how to fix + ]; + + + NSMutableArray* cacheDirURLs = [NSMutableArray arrayWithCapacity: cacheDirPathComponents.count * homeDirectoryURLs.count]; + for (NSURL* homeDirURL in homeDirectoryURLs) { + for (NSString* cacheDirPathComponent in cacheDirPathComponents) { + [cacheDirURLs addObject: [homeDirURL URLByAppendingPathComponent: cacheDirPathComponent isDirectory: YES]]; + } + } + + for (NSURL* cacheDirURL in cacheDirURLs) { + NSLog(@"Clearing browser cache folder %@", cacheDirURL); + // removeItemAtURL will return errors if the file doesn't exist + // so we don't track the errors - best effort is OK + [fileManager removeItemAtURL: cacheDirURL error: nil]; + } + + return nil; +} + ++ (void)clearOSDNSCache { + // no error checks - if it works it works! + NSTask* flushDsCacheUtil = [[NSTask alloc] init]; + [flushDsCacheUtil setLaunchPath: @"/usr/bin/dscacheutil"]; + [flushDsCacheUtil setArguments: @[@"-flushcache"]]; + [flushDsCacheUtil launch]; + [flushDsCacheUtil waitUntilExit]; + + NSTask* killResponder = [[NSTask alloc] init]; + [killResponder setLaunchPath: @"/usr/bin/killall"]; + [killResponder setArguments: @[@"-HUP", @"mDNSResponder"]]; + [killResponder launch]; + [killResponder waitUntilExit]; + + NSTask* killResponderHelper = [[NSTask alloc] init]; + [killResponderHelper setLaunchPath: @"/usr/bin/killall"]; + [killResponderHelper setArguments: @[@"mDNSResponderHelper"]]; + [killResponderHelper launch]; + [killResponderHelper waitUntilExit]; + + NSLog(@"Cleared OS DNS caches"); +} + ++ (void)removeBlock { + SCSettings* settings = [SCSettings sharedSettings]; + + [SCBlockUtilities removeBlockFromSettings]; + [SCHelperToolUtilities uninstallBlockRules]; + + // always synchronize settings ASAP after removing a block to let everybody else know + [settings synchronizeSettings]; + + // let the main app know things have changed so it can update the UI! + [SCHelperToolUtilities sendConfigurationChangedNotification]; + + NSLog(@"INFO: Block cleared."); + + [SCHelperToolUtilities clearCachesIfRequested]; +} + ++ (void)sendConfigurationChangedNotification { + // if you don't include the NSNotificationPostToAllSessions option, + // it will not deliver when run by launchd (root) to the main app being run by the user + [[NSDistributedNotificationCenter defaultCenter] postNotificationName: @"SCConfigurationChangedNotification" + object: nil + userInfo: nil + options: NSNotificationDeliverImmediately | NSNotificationPostToAllSessions]; +} + +@end diff --git a/Common/Utility/SCMiscUtilities.h b/Common/Utility/SCMiscUtilities.h index a59b976a..b5aa6c9c 100644 --- a/Common/Utility/SCMiscUtilities.h +++ b/Common/Utility/SCMiscUtilities.h @@ -21,8 +21,6 @@ + (NSArray*)allUserHomeDirectoryURLs:(NSError**)errPtr; -+ (NSError*)clearBrowserCaches; - + (BOOL)errorIsAuthCanceled:(NSError*)err; diff --git a/Common/Utility/SCMiscUtilities.m b/Common/Utility/SCMiscUtilities.m index 551fb0d1..9edd1d41 100644 --- a/Common/Utility/SCMiscUtilities.m +++ b/Common/Utility/SCMiscUtilities.m @@ -5,7 +5,7 @@ // Created by Charles Stigler on 07/07/2018. // -#import "HelperCommon.h" +#import "SCHelperToolUtilities.h" #import "SCSettings.h" @implementation SCMiscUtilities @@ -181,42 +181,4 @@ + (BOOL)errorIsAuthCanceled:(NSError*)err { return homeDirectoryURLs; } -+ (NSError*)clearBrowserCaches { - NSFileManager* fileManager = [NSFileManager defaultManager]; - - NSError* homeDirErr = nil; - NSArray* homeDirectoryURLs = [SCMiscUtilities allUserHomeDirectoryURLs: &homeDirErr]; - if (homeDirectoryURLs == nil) return homeDirErr; - - NSArray* cacheDirPathComponents = @[ - // chrome - @"/Library/Caches/Google/Chrome/Default", - @"/Library/Caches/Google/Chrome/com.google.Chrome", - - // firefox - @"/Library/Caches/Firefox/Profiles", - - // safari - @"/Library/Caches/com.apple.Safari", - @"/Library/Containers/com.apple.Safari/Data/Library/Caches" // this one seems to fail due to permissions issues, but not sure how to fix - ]; - - - NSMutableArray* cacheDirURLs = [NSMutableArray arrayWithCapacity: cacheDirPathComponents.count * homeDirectoryURLs.count]; - for (NSURL* homeDirURL in homeDirectoryURLs) { - for (NSString* cacheDirPathComponent in cacheDirPathComponents) { - [cacheDirURLs addObject: [homeDirURL URLByAppendingPathComponent: cacheDirPathComponent isDirectory: YES]]; - } - } - - for (NSURL* cacheDirURL in cacheDirURLs) { - NSLog(@"Clearing browser cache folder %@", cacheDirURL); - // removeItemAtURL will return errors if the file doesn't exist - // so we don't track the errors - best effort is OK - [fileManager removeItemAtURL: cacheDirURL error: nil]; - } - - return nil; -} - @end diff --git a/Common/Utility/SCUtility.h b/Common/Utility/SCUtility.h index 6f67ca34..d30bcf4a 100644 --- a/Common/Utility/SCUtility.h +++ b/Common/Utility/SCUtility.h @@ -11,5 +11,6 @@ #import "SCMigrationUtilities.h" #import "SCBlockUtilities.h" #import "SCMiscUtilities.h" +#import "SCHelperToolUtilities.h" #endif /* SCUtility_h */ diff --git a/Daemon/DaemonMain.m b/Daemon/DaemonMain.m index dcb44bb3..43a89ff3 100644 --- a/Daemon/DaemonMain.m +++ b/Daemon/DaemonMain.m @@ -8,6 +8,7 @@ #import #import "SCDaemon.h" +// Entry point for the SelfControl daemon process (selfcontrold) int main(int argc, const char *argv[]) { [SCSentry startSentry: @"org.eyebeam.selfcontrold"]; diff --git a/Daemon/SCDaemon.h b/Daemon/SCDaemon.h index 3f89e9c3..1e857311 100644 --- a/Daemon/SCDaemon.h +++ b/Daemon/SCDaemon.h @@ -9,10 +9,16 @@ NS_ASSUME_NONNULL_BEGIN +// SCDaemon is the top-level class that runs the SelfControl +// daemon process (selfcontrold). It runs from DaemonMain. @interface SCDaemon : NSObject +// Singleton instance of SCDaemon + (instancetype)sharedDaemon; + +// Starts the daemon tasks, including accepting XPC connections +// and running block checkup jobs if necessary - (void)start; // Starts checking up on the block on a regular basis diff --git a/Daemon/SCDaemon.m b/Daemon/SCDaemon.m index 70dfb1a2..3f7e7a44 100644 --- a/Daemon/SCDaemon.m +++ b/Daemon/SCDaemon.m @@ -10,7 +10,6 @@ #import "SCDaemonXPC.h" #import"SCDaemonBlockMethods.h" #import "HostFileBlocker.h" -#import "SCDaemonUtilities.h" static NSString* serviceName = @"org.eyebeam.selfcontrold"; float const INACTIVITY_LIMIT_SECS = 60 * 2; // 2 minutes @@ -112,7 +111,7 @@ - (void)startInactivityTimer { } NSLog(@"Daemon inactive for more than %f seconds, exiting!", INACTIVITY_LIMIT_SECS); - [SCDaemonUtilities unloadDaemonJob]; + [SCHelperToolUtilities unloadDaemonJob]; } }]; } diff --git a/Daemon/SCDaemonBlockMethods.h b/Daemon/SCDaemonBlockMethods.h index f16b7b3c..72bfede8 100644 --- a/Daemon/SCDaemonBlockMethods.h +++ b/Daemon/SCDaemonBlockMethods.h @@ -9,16 +9,24 @@ NS_ASSUME_NONNULL_BEGIN +// Top-level logic for different methods run by the SelfControl daemon +// these logics can be run by XPC methods, or elsewhere @interface SCDaemonBlockMethods : NSObject @property (class, readonly) NSLock* daemonMethodLock; +// Starts a block + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate blockSettings:(NSDictionary*)blockSettings authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +// Checks whether the block is expired or compromised, and takes action to fix + (void)checkupBlock; +// updates the blocklist for the currently running block +// (i.e. adds new sites to the list) + (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +// updates the block end date for the currently running block +// (i.e. extends the block) + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; @end diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index 79ac58d0..e12e6c5a 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -7,11 +7,11 @@ #import "SCDaemonBlockMethods.h" #import "SCSettings.h" -#import "HelperCommon.h" +#import "SCHelperToolUtilities.h" #import "PacketFilter.h" -#import "SCDaemonUtilities.h" #import "BlockManager.h" #import "SCDaemon.h" +#import "LaunchctlHelper.h" NSTimeInterval METHOD_LOCK_TIMEOUT = 5.0; NSTimeInterval CHECKUP_LOCK_TIMEOUT = 0.5; // use a shorter lock timeout for checkups, because we'd prefer not to have tons pile up @@ -106,17 +106,17 @@ + (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)newBlocklist authorization:(NSData [settings setValue: newBlocklist forKey: @"ActiveBlocklist"]; [settings synchronizeSettings]; // make sure everyone knows about our new list - sendConfigurationChangedNotification(); + [SCHelperToolUtilities sendConfigurationChangedNotification]; // Clear all caches if the user has the correct preference set, so // that blocked pages are not loaded from a cache. - clearCachesIfRequested(); + [SCHelperToolUtilities clearCachesIfRequested]; [SCSentry addBreadcrumb: @"Daemon updated blocklist successfully" category: @"daemon"]; NSLog(@"INFO: Blocklist successfully updated."); @@ -245,7 +245,7 @@ + (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData [settings setValue: newEndDate forKey: @"BlockEndDate"]; [settings synchronizeSettings]; // make sure everyone knows about our new end date - sendConfigurationChangedNotification(); + [SCHelperToolUtilities sendConfigurationChangedNotification]; [SCSentry addBreadcrumb: @"Daemon extended block successfully" category: @"daemon"]; NSLog(@"INFO: Block successfully extended."); @@ -279,9 +279,9 @@ + (void)checkupBlock { [SCSentry captureMessage: @"Checkup ran and no active block found! Removing block, tampering suspected..."]; - removeBlock(); + [SCHelperToolUtilities removeBlock]; - sendConfigurationChangedNotification(); + [SCHelperToolUtilities sendConfigurationChangedNotification]; // Temporarily disabled the TamperingDetection flag because it was sometimes causing false positives // (i.e. people having the background set repeatedly despite no attempts to cheat) @@ -296,9 +296,9 @@ + (void)checkupBlock { } else if ([SCBlockUtilities currentBlockIsExpired]) { NSLog(@"INFO: Checkup ran, block expired, removing block."); - removeBlock(); + [SCHelperToolUtilities removeBlock]; - sendConfigurationChangedNotification(); + [SCHelperToolUtilities sendConfigurationChangedNotification]; [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; @@ -336,9 +336,9 @@ + (void)checkupBlock { [hostFileBlocker deleteBackupHostsFile]; // Perform the re-add of the rules - addRulesToFirewall(); + [SCHelperToolUtilities installBlockRulesFromSettings]; - clearCachesIfRequested(); + [SCHelperToolUtilities clearCachesIfRequested]; [SCSentry addBreadcrumb: @"Daemon found compromised block integrity and re-added rules" category: @"daemon"]; NSLog(@"INFO: Checkup ran, readded block rules."); diff --git a/Daemon/SCDaemonProtocol.h b/Daemon/SCDaemonProtocol.h index 8e5261c6..07ccbdef 100644 --- a/Daemon/SCDaemonProtocol.h +++ b/Daemon/SCDaemonProtocol.h @@ -11,14 +11,16 @@ NS_ASSUME_NONNULL_BEGIN @protocol SCDaemonProtocol +// XPC method to start block - (void)startBlockWithControllingUID:(uid_t)controllingUID blocklist:(NSArray*)blocklist isAllowlist:(BOOL)isAllowlist endDate:(NSDate*)endDate blockSettings:(NSDictionary*)blockSettings authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +// XPC method to add to blocklist - (void)updateBlocklist:(NSArray*)newBlocklist authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; +// XPC method to extend block - (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData reply:(void(^)(NSError* error))reply; -- (BOOL) checkup; - +// XPC method to get version of the installed daemon - (void)getVersionWithReply:(void(^)(NSString * version))reply; @end diff --git a/Daemon/SCDaemonUtilities.h b/Daemon/SCDaemonUtilities.h deleted file mode 100644 index 4eabfb3c..00000000 --- a/Daemon/SCDaemonUtilities.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// SCDaemonUtilities.h -// org.eyebeam.selfcontrold -// -// Created by Charlie Stigler on 9/16/20. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SCDaemonUtilities : NSObject - -+ (void)unloadDaemonJob; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Daemon/SCDaemonUtilities.m b/Daemon/SCDaemonUtilities.m deleted file mode 100644 index 2ac0f4a4..00000000 --- a/Daemon/SCDaemonUtilities.m +++ /dev/null @@ -1,39 +0,0 @@ -// -// SCDaemonUtilities.m -// org.eyebeam.selfcontrold -// -// Created by Charlie Stigler on 9/16/20. -// - -#import "SCDaemonUtilities.h" -#import -#import "SCSettings.h" -#import "BlockManager.h" - -@implementation SCDaemonUtilities - -+ (void)unloadDaemonJob { - NSLog(@"Unloading SelfControl daemon..."); - [SCSentry addBreadcrumb: @"Daemon about to unload" category: @"daemon"]; - SCSettings* settings = [SCSettings sharedSettings]; - - // we're about to unload the launchd job - // this will kill this process, so we have to make sure - // all settings are synced before we unload - NSError* syncErr; - [settings syncSettingsAndWait: 5.0 error: &syncErr]; - if (syncErr != nil) { - NSLog(@"WARNING: Sync failed or timed out with error %@ before unloading daemon job", syncErr); - [SCSentry captureError: syncErr]; - } - - // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. - CFErrorRef cfError; - // this should block until the process is dead, so we should never get to the other side if it's successful - SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, YES, &cfError); - if (cfError) { - NSLog(@"Failed to remove selfcontrold daemon with error %@", cfError); - } -} - -@end diff --git a/Daemon/SCDaemonXPC.h b/Daemon/SCDaemonXPC.h index 428e1785..720d6acf 100644 --- a/Daemon/SCDaemonXPC.h +++ b/Daemon/SCDaemonXPC.h @@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN +// Implementations for SC XPC methods +// (see SCDaemonProtocol for all method prototypes) @interface SCDaemonXPC : NSObject @end diff --git a/Daemon/SCDaemonXPC.m b/Daemon/SCDaemonXPC.m index 0d25033c..ad9f09c1 100644 --- a/Daemon/SCDaemonXPC.m +++ b/Daemon/SCDaemonXPC.m @@ -65,14 +65,6 @@ - (void)updateBlockEndDate:(NSDate*)newEndDate authorization:(NSData *)authData [SCDaemonBlockMethods updateBlockEndDate: newEndDate authorization: authData reply: reply]; } -- (BOOL) checkup { - NSLog(@"XPC method called: checkup"); - - // no authorization needed to run a checkup - - return YES; -} - // Part of the HelperToolProtocol. Returns the version number of the tool. Note that never // requires authorization. - (void)getVersionWithReply:(void(^)(NSString * version))reply { diff --git a/NSString+IPAddress.h b/NSString+IPAddress.h index b9f1bc21..36c52360 100644 --- a/NSString+IPAddress.h +++ b/NSString+IPAddress.h @@ -23,6 +23,7 @@ #import #include +// methods to check whether the string is a valid IP address @interface NSString (IPAddress) @property (nonatomic, getter=isValidIPv4Address, readonly) BOOL validIPv4Address; diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index 0bc101ac..11dad2b2 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -11,9 +11,10 @@ #import #import "BlockManager.h" #import "SCSettings.h" -#import "HelperCommon.h" +#import "SCHelperToolUtilities.h" #import #import "SCMigrationUtilities.h" +#import #define LOG_FILE @"~/Documents/SelfControl-Killer.log" @@ -236,7 +237,7 @@ int main(int argc, char* argv[]) { // let the main app know to refresh - sendConfigurationChangedNotification(); + [SCHelperToolUtilities sendConfigurationChangedNotification]; exit(EX_OK); } diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 5da50a6c..c5f2892e 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -99,8 +99,12 @@ CB81A9F625B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; CB81A9F725B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; CB81A9F825B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */; }; + CB81AA3A25B7D152006956F7 /* SCHelperToolUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = CB81AA3825B7D152006956F7 /* SCHelperToolUtilities.h */; }; + CB81AA3C25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; + CB81AA3E25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; + CB81AA3F25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; + CB81AA4025B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; CB850F3925130F5300EE2E2D /* NSString+IPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806516C237F10059C99A /* NSString+IPAddress.m */; }; - CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */; }; CB90BF830F49F430006D202D /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; CB9365620F8581B000EF284E /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CB9365610F8581B000EF284E /* dsa_pub.pem */; }; CB9366E80F85BEF100EF284E /* NSRemoveTemplate.jpg in Resources */ = {isa = PBXBuildFile; fileRef = CB9366E60F85BEF100EF284E /* NSRemoveTemplate.jpg */; }; @@ -290,8 +294,8 @@ CB81A9E225B7C2A8006956F7 /* SCUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCUtility.h; sourceTree = ""; }; CB81A9F025B7C5F7006956F7 /* SCBlockFileReaderWriter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCBlockFileReaderWriter.h; sourceTree = ""; }; CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCBlockFileReaderWriter.m; sourceTree = ""; }; - CB850F3A2513185300EE2E2D /* SCDaemonUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCDaemonUtilities.h; sourceTree = ""; }; - CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCDaemonUtilities.m; sourceTree = ""; }; + CB81AA3825B7D152006956F7 /* SCHelperToolUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCHelperToolUtilities.h; sourceTree = ""; }; + CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCHelperToolUtilities.m; sourceTree = ""; }; CB90BF810F49F430006D202D /* HostImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostImporter.h; sourceTree = ""; }; CB90BF820F49F430006D202D /* HostImporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HostImporter.m; sourceTree = ""; }; CB9365610F8581B000EF284E /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; @@ -698,8 +702,6 @@ CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */, CB8086D224837607004B88BD /* SCDaemonXPC.h */, CB8086D324837607004B88BD /* SCDaemonXPC.m */, - CB850F3A2513185300EE2E2D /* SCDaemonUtilities.h */, - CB850F3B2513185300EE2E2D /* SCDaemonUtilities.m */, CB74D122248374E6002B2079 /* SCDaemonProtocol.h */, CB62FC2F24B11A4F00ADBC40 /* selfcontrold-Info.plist */, CB8086D524837734004B88BD /* org.eyebeam.selfcontrold.plist */, @@ -717,6 +719,8 @@ CB81A9CE25B7C269006956F7 /* SCBlockUtilities.m */, CBB1731220F041F4007FCAE9 /* SCMiscUtilities.h */, CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */, + CB81AA3825B7D152006956F7 /* SCHelperToolUtilities.h */, + CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */, ); path = Utility; sourceTree = ""; @@ -805,6 +809,7 @@ buildActionMask = 2147483647; files = ( CB81A94825B7B5B5006956F7 /* SCMigrationUtilities.h in Headers */, + CB81AA3A25B7D152006956F7 /* SCHelperToolUtilities.h in Headers */, CB81A9CF25B7C269006956F7 /* SCBlockUtilities.h in Headers */, CB81A9F225B7C5F7006956F7 /* SCBlockFileReaderWriter.h in Headers */, ); @@ -1310,6 +1315,7 @@ files = ( CBDF919A225C5A9700358B95 /* SCMiscUtilities.m in Sources */, CB5DFCBC2251DD1F0084CEC2 /* SCConstants.m in Sources */, + CB81AA3F25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, CB81A9D425B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB81A9F725B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CB114284222CD4F0004B7868 /* SCSettings.m in Sources */, @@ -1333,11 +1339,11 @@ CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */, CB62FC3F24B1327A00ADBC40 /* SCMiscUtilities.m in Sources */, CB1465BC25B027E700130D2E /* SCErr.m in Sources */, + CB81AA4025B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, CB62FC4924B1330700ADBC40 /* LaunchctlHelper.m in Sources */, CB62FC4824B132B300ADBC40 /* SCConstants.m in Sources */, CB81A9D525B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */, - CB850F3C2513185300EE2E2D /* SCDaemonUtilities.m in Sources */, CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */, CB81A94E25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, @@ -1378,6 +1384,7 @@ CBE44FEB19E50900004E9706 /* AllowlistScraper.m in Sources */, CB9C812619CFBB5E00CDCAE1 /* BlockManager.m in Sources */, CB9C812519CFBB5500CDCAE1 /* HelperCommon.m in Sources */, + CB81AA3E25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, CB9C812419CFBB4E00CDCAE1 /* HostFileBlocker.m in Sources */, CB81A94C25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CB1CA64F25ABA5BB0084A551 /* SCXPCClient.m in Sources */, @@ -1402,6 +1409,7 @@ CB20C5D8245699D700B9D749 /* version-header.h in Sources */, CBA2AFD90F39EC46005AFEBE /* cli-main.m in Sources */, CB5DFCB82251DD1F0084CEC2 /* SCConstants.m in Sources */, + CB81AA3C25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, CB81A9F425B7C5F7006956F7 /* SCBlockFileReaderWriter.m in Sources */, CBCA91121960D87300AFD20C /* PacketFilter.m in Sources */, CB73616219E5086A00E0924F /* AllowlistScraper.m in Sources */, diff --git a/cli-main.m b/cli-main.m index becf1762..e3d09085 100755 --- a/cli-main.m +++ b/cli-main.m @@ -21,10 +21,11 @@ // along with this program. If not, see . #import "PacketFilter.h" -#import "HelperCommon.h" +#import "SCHelperToolUtilities.h" #import "SCSettings.h" #import "SCXPCClient.h" #import "SCBlockFileReaderWriter.h" +#import // The main method which deals which most of the logic flow and execution of // the CLI tool. From 668bca3bc0d4f32d5b8d0398daf5f0210c20605c Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 19:20:28 -0800 Subject: [PATCH 58/72] minor refactoring on helper tool utility functions --- Common/HelperCommon.h | 56 ------------ Common/HelperCommon.m | 122 ------------------------- Common/SCSettings.h | 2 +- Common/SCSettings.m | 9 +- Common/Utility/SCHelperToolUtilities.h | 3 - Common/Utility/SCHelperToolUtilities.m | 61 ++++++------- SCKillerHelper/main.m | 3 +- SelfControl.xcodeproj/project.pbxproj | 10 -- 8 files changed, 36 insertions(+), 230 deletions(-) delete mode 100644 Common/HelperCommon.h delete mode 100644 Common/HelperCommon.m diff --git a/Common/HelperCommon.h b/Common/HelperCommon.h deleted file mode 100644 index fd1b419f..00000000 --- a/Common/HelperCommon.h +++ /dev/null @@ -1,56 +0,0 @@ -// -// HelperCommonFunctions.h -// SelfControl -// -// Created by Charlie Stigler on 07/13/09. -// Copyright 2009 Eyebeam. - -// This file is part of SelfControl. -// -// SelfControl is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// All the functions etc. formerly used for the helper tool are now separated into this file. -// This is so that another helper tool can be created that easily uses those same functions. - -// imports for all helper tools -#import -#import -#import "LaunchctlHelper.h" -#import -#import -#import -#import "HostFileBlocker.h" -#import "SCSettings.h" - -// Reads the domain block list from the settings for SelfControl, and adds deny -// rules for all of the IPs (or the A DNS record IPS for doamin names) to the -// ipfw firewall. -void installBlockRulesFromSettings(void); - -// Removes from ipfw all rules that were created by SelfControl. -void uninstallBlockRules(void); - -// Checks the settings system to see whether the user wants their web browser -// caches cleared, and deletes the specific cache folders for a few common -// web browsers if it is required. -void clearCachesIfRequested(void); - -// Clear only the caches for browsers -void clearBrowserCaches(void); - -// Clear only the OS-level DNS cache -void clearOSDNSCache(void); - -// Removes block via settings, host file rules and ipfw rules, -// deleting user caches if requested, and migrating legacy settings. -void removeBlock(void); - -void sendConfigurationChangedNotification(void); diff --git a/Common/HelperCommon.m b/Common/HelperCommon.m deleted file mode 100644 index 55cbb87e..00000000 --- a/Common/HelperCommon.m +++ /dev/null @@ -1,122 +0,0 @@ -/* - * HelperCommonFunctions.c - * SelfControl - * - * Created by Charlie Stigler on 7/13/10. - * Copyright 2010 Harvard-Westlake Student. All rights reserved. - * - */ - -#include "HelperCommon.h" -#include "BlockManager.h" -#import "SCSettings.h" -#import - -void installBlockRulesFromSettings() { - SCSettings* settings = [SCSettings sharedSettings]; - BOOL shouldEvaluateCommonSubdomains = [settings boolForKey: @"EvaluateCommonSubdomains"]; - BOOL allowLocalNetworks = [settings boolForKey: @"AllowLocalNetworks"]; - BOOL includeLinkedDomains = [settings boolForKey: @"IncludeLinkedDomains"]; - - // get value for ActiveBlockAsWhitelist - BOOL blockAsAllowlist = [settings boolForKey: @"ActiveBlockAsWhitelist"]; - - BlockManager* blockManager = [[BlockManager alloc] initAsAllowlist: blockAsAllowlist allowLocal: allowLocalNetworks includeCommonSubdomains: shouldEvaluateCommonSubdomains includeLinkedDomains: includeLinkedDomains]; - - NSLog(@"About to run BlockManager commands"); - - [blockManager prepareToAddBlock]; - [blockManager addBlockEntries: [settings valueForKey: @"ActiveBlocklist"]]; - [blockManager finalizeBlock]; - -} - -void uninstallBlockRules() { - // options don't really matter because we're only using it to clear - BlockManager* blockManager = [[BlockManager alloc] init]; - [blockManager clearBlock]; - - // We'll play the sound now rather than earlier, because - // it is important that the UI get updated (by the posted - // notification) before we sleep to play the sound. Otherwise, - // the app seems unresponsive and slow. - SCSettings* settings = [SCSettings sharedSettings]; - if([settings boolForKey: @"BlockSoundShouldPlay"]) { - // Map the tags used in interface builder to the sound - NSArray* systemSoundNames = SCConstants.systemSoundNames; - NSSound* alertSound = [NSSound soundNamed: systemSoundNames[[[settings valueForKey: @"BlockSound"] intValue]]]; - if(!alertSound) - NSLog(@"WARNING: Alert sound not found."); - else { - [alertSound play]; - // Sleeping a second is a messy way of doing this, but otherwise the - // sound is killed along with this process when it is unloaded in just - // a few lines. - sleep(1); - } - } -} - -void clearCachesIfRequested() { - SCSettings* settings = [SCSettings sharedSettings]; - if(![settings boolForKey: @"ClearCaches"]) { - return; - } - - NSError* err = [SCHelperToolUtilities clearBrowserCaches]; - if (err) { - NSLog(@"WARNING: Error clearing browser caches: %@", err); - [SCSentry captureError: err]; - } - - [SCHelperToolUtilities clearOSDNSCache]; -} - -void clearOSDNSCache() { - // no error checks - if it works it works! - NSTask* flushDsCacheUtil = [[NSTask alloc] init]; - [flushDsCacheUtil setLaunchPath: @"/usr/bin/dscacheutil"]; - [flushDsCacheUtil setArguments: @[@"-flushcache"]]; - [flushDsCacheUtil launch]; - [flushDsCacheUtil waitUntilExit]; - - NSTask* killResponder = [[NSTask alloc] init]; - [killResponder setLaunchPath: @"/usr/bin/killall"]; - [killResponder setArguments: @[@"-HUP", @"mDNSResponder"]]; - [killResponder launch]; - [killResponder waitUntilExit]; - - NSTask* killResponderHelper = [[NSTask alloc] init]; - [killResponderHelper setLaunchPath: @"/usr/bin/killall"]; - [killResponderHelper setArguments: @[@"mDNSResponderHelper"]]; - [killResponderHelper launch]; - [killResponderHelper waitUntilExit]; - - NSLog(@"Cleared OS DNS caches"); -} - -void removeBlock() { - SCSettings* settings = [SCSettings sharedSettings]; - - [SCBlockUtilities removeBlockFromSettings]; - [SCHelperToolUtilities uninstallBlockRules]; - - // always synchronize settings ASAP after removing a block to let everybody else know - [settings synchronizeSettings]; - - // let the main app know things have changed so it can update the UI! - [SCHelperToolUtilities sendConfigurationChangedNotification]; - - NSLog(@"INFO: Block cleared."); - - [SCHelperToolUtilities clearCachesIfRequested]; -} - -void sendConfigurationChangedNotification() { - // if you don't include the NSNotificationPostToAllSessions option, - // it will not deliver when run by launchd (root) to the main app being run by the user - [[NSDistributedNotificationCenter defaultCenter] postNotificationName: @"SCConfigurationChangedNotification" - object: nil - userInfo: nil - options: NSNotificationDeliverImmediately | NSNotificationPostToAllSessions]; -} diff --git a/Common/SCSettings.h b/Common/SCSettings.h index 10ebcf7b..26f87808 100644 --- a/Common/SCSettings.h +++ b/Common/SCSettings.h @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)writeSettings; - (void)synchronizeSettingsWithCompletion:(nullable void(^)(NSError* _Nullable))completionBlock; - (void)synchronizeSettings; -- (void)syncSettingsAndWait:(int)timeoutSecs error:(NSError* __strong *)errPtr; +- (NSError*)syncSettingsAndWait:(int)timeoutSecs; - (void)setValue:(id)value forKey:(NSString*)key stopPropagation:(BOOL)stopPropagation; - (void)setValue:(nullable id)value forKey:(NSString*)key; diff --git a/Common/SCSettings.m b/Common/SCSettings.m index 2ad85015..a80eac50 100644 --- a/Common/SCSettings.m +++ b/Common/SCSettings.m @@ -358,22 +358,25 @@ - (void)synchronizeSettings { [self synchronizeSettingsWithCompletion: nil]; } -- (void)syncSettingsAndWait:(int)timeoutSecs error:(NSError* __strong *)errPtr { +- (NSError*)syncSettingsAndWait:(int)timeoutSecs { dispatch_semaphore_t sema = dispatch_semaphore_create(0); + __block NSError* retErr = nil; // do this on another thread so it doesn't deadlock our semaphore // (also dispatch_async ensures correct behavior even if synchronizeSettingsWithCompletion itself returns synchronously) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self synchronizeSettingsWithCompletion:^(NSError* err) { - *errPtr = err; + retErr = err; dispatch_semaphore_signal(sema); }]; }); if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, timeoutSecs * NSEC_PER_SEC))) { - *errPtr = [SCErr errorWithCode: 601]; + retErr = [SCErr errorWithCode: 601]; } + + return retErr; } - (void)setValue:(id)value forKey:(NSString*)key stopPropagation:(BOOL)stopPropagation { diff --git a/Common/Utility/SCHelperToolUtilities.h b/Common/Utility/SCHelperToolUtilities.h index f51da13f..3efe4a09 100644 --- a/Common/Utility/SCHelperToolUtilities.h +++ b/Common/Utility/SCHelperToolUtilities.h @@ -19,9 +19,6 @@ NS_ASSUME_NONNULL_BEGIN // ipfw firewall. + (void)installBlockRulesFromSettings; -// Removes from ipfw all rules that were created by SelfControl. -+ (void)uninstallBlockRules; - // calls SMJobRemove to unload the daemon from launchd // (which also kills the running process, synchronously) + (void)unloadDaemonJob; diff --git a/Common/Utility/SCHelperToolUtilities.m b/Common/Utility/SCHelperToolUtilities.m index 7751e072..813ff714 100644 --- a/Common/Utility/SCHelperToolUtilities.m +++ b/Common/Utility/SCHelperToolUtilities.m @@ -30,32 +30,6 @@ + (void)installBlockRulesFromSettings { } -+ (void)uninstallBlockRules { - // options don't really matter because we're only using it to clear - BlockManager* blockManager = [[BlockManager alloc] init]; - [blockManager clearBlock]; - - // We'll play the sound now rather than earlier, because - // it is important that the UI get updated (by the posted - // notification) before we sleep to play the sound. Otherwise, - // the app seems unresponsive and slow. - SCSettings* settings = [SCSettings sharedSettings]; - if([settings boolForKey: @"BlockSoundShouldPlay"]) { - // Map the tags used in interface builder to the sound - NSArray* systemSoundNames = SCConstants.systemSoundNames; - NSSound* alertSound = [NSSound soundNamed: systemSoundNames[[[settings valueForKey: @"BlockSound"] intValue]]]; - if(!alertSound) - NSLog(@"WARNING: Alert sound not found."); - else { - [alertSound play]; - // Sleeping a second is a messy way of doing this, but otherwise the - // sound is killed along with this process when it is unloaded in just - // a few lines. - sleep(1); - } - } -} - + (void)unloadDaemonJob { NSLog(@"Unloading SelfControl daemon..."); [SCSentry addBreadcrumb: @"Daemon about to unload" category: @"daemon"]; @@ -64,8 +38,7 @@ + (void)unloadDaemonJob { // we're about to unload the launchd job // this will kill this process, so we have to make sure // all settings are synced before we unload - NSError* syncErr; - [settings syncSettingsAndWait: 5.0 error: &syncErr]; + NSError* syncErr = [settings syncSettingsAndWait: 5.0]; if (syncErr != nil) { NSLog(@"WARNING: Sync failed or timed out with error %@ before unloading daemon job", syncErr); [SCSentry captureError: syncErr]; @@ -156,21 +129,43 @@ + (void)clearOSDNSCache { NSLog(@"Cleared OS DNS caches"); } ++ (void)playBlockEndSound { + SCSettings* settings = [SCSettings sharedSettings]; + if([settings boolForKey: @"BlockSoundShouldPlay"]) { + // Map the tags used in interface builder to the sound + NSArray* systemSoundNames = SCConstants.systemSoundNames; + NSSound* alertSound = [NSSound soundNamed: systemSoundNames[[[settings valueForKey: @"BlockSound"] intValue]]]; + if(!alertSound) + NSLog(@"WARNING: Alert sound not found."); + else { + [alertSound play]; + } + } +} + + (void)removeBlock { SCSettings* settings = [SCSettings sharedSettings]; - [SCBlockUtilities removeBlockFromSettings]; - [SCHelperToolUtilities uninstallBlockRules]; + [[BlockManager new] clearBlock]; + + [SCHelperToolUtilities clearCachesIfRequested]; + + // play a sound letting + [SCHelperToolUtilities playBlockEndSound]; // always synchronize settings ASAP after removing a block to let everybody else know - [settings synchronizeSettings]; + // and wait until they're synced before we send the configuration change notification + // so the app has no chance of reading the data before we update it + NSError* syncErr = [settings syncSettingsAndWait: 5.0]; + if (syncErr != nil) { + NSLog(@"WARNING: Sync failed or timed out with error %@ after removing block", syncErr); + [SCSentry captureError: syncErr]; + } // let the main app know things have changed so it can update the UI! [SCHelperToolUtilities sendConfigurationChangedNotification]; NSLog(@"INFO: Block cleared."); - - [SCHelperToolUtilities clearCachesIfRequested]; } + (void)sendConfigurationChangedNotification { diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index 11dad2b2..4741408a 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -221,8 +221,7 @@ int main(int argc, char* argv[]) { } // OK, make sure all settings are synced before this thing exits - NSError* syncSettingsErr = nil; - [settings syncSettingsAndWait: 5 error: &syncSettingsErr]; + NSError* syncSettingsErr = [settings syncSettingsAndWait: 5]; if (syncSettingsErr != nil) { [log appendFormat: @"\nWARNING: Settings failed to synchronize before exit, with error %@", syncSettingsErr]; diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index c5f2892e..e64a8b51 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = CB62FC3D24B1298500ADBC40 /* SCDaemonBlockMethods.m */; }; CB62FC3F24B1327A00ADBC40 /* SCMiscUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB1731320F041F4007FCAE9 /* SCMiscUtilities.m */; }; CB62FC4024B1327D00ADBC40 /* SCSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = CBF3B573217BADD7006D5F52 /* SCSettings.m */; }; - CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */; }; CB62FC4224B1329200ADBC40 /* BlockManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806116C1FDBE0059C99A /* BlockManager.m */; }; CB62FC4324B1329500ADBC40 /* PacketFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = CBCA91111960D87300AFD20C /* PacketFilter.m */; }; CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; @@ -116,7 +115,6 @@ CB9C812219CFBB3800CDCAE1 /* PacketFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = CBCA91111960D87300AFD20C /* PacketFilter.m */; }; CB9C812319CFBB4400CDCAE1 /* LaunchctlHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */; }; CB9C812419CFBB4E00CDCAE1 /* HostFileBlocker.m in Sources */ = {isa = PBXBuildFile; fileRef = CBB0AE290FA74566006229B3 /* HostFileBlocker.m */; }; - CB9C812519CFBB5500CDCAE1 /* HelperCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */; }; CB9C812619CFBB5E00CDCAE1 /* BlockManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806116C1FDBE0059C99A /* BlockManager.m */; }; CB9C812719CFBB6400CDCAE1 /* NSString+IPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806516C237F10059C99A /* NSString+IPAddress.m */; }; CB9C812819CFBB7B00CDCAE1 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E901D0F397FFA006DE6E4 /* Security.framework */; }; @@ -146,7 +144,6 @@ CBBF4EE515830D7300E364D9 /* TimerWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CBBF4EE715830D7300E364D9 /* TimerWindow.xib */; }; CBC2F8580F4672FE00CF2A42 /* LaunchctlHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CBC2F8570F4672FE00CF2A42 /* LaunchctlHelper.m */; }; CBCA91121960D87300AFD20C /* PacketFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = CBCA91111960D87300AFD20C /* PacketFilter.m */; }; - CBD266B011ED7D9C00042CD8 /* HelperCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */; }; CBD2677011ED92DE00042CD8 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB9E90190F397FF6006DE6E4 /* CoreFoundation.framework */; }; CBD2677311ED92EF00042CD8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B97325FDCFA39411CA2CEA /* Foundation.framework */; }; CBD2677511ED92F800042CD8 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; @@ -343,8 +340,6 @@ CBCA91111960D87300AFD20C /* PacketFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PacketFilter.m; sourceTree = ""; }; CBCA91271961381F00AFD20C /* tr */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CBCA912B1961384600AFD20C /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; - CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HelperCommon.h; sourceTree = ""; }; - CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HelperCommon.m; sourceTree = ""; }; CBD4848219D75F6D0020F949 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sparkle.framework; sourceTree = ""; }; CBD4848519D7611F0020F949 /* Podfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; CBD4848619D7611F0020F949 /* TemplateIcon2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TemplateIcon2x.png; sourceTree = ""; }; @@ -631,8 +626,6 @@ CB1465B725B027E700130D2E /* SCErr.m */, CBF3B572217BADD7006D5F52 /* SCSettings.h */, CBF3B573217BADD7006D5F52 /* SCSettings.m */, - CBD266AD11ED7D9C00042CD8 /* HelperCommon.h */, - CBD266AE11ED7D9C00042CD8 /* HelperCommon.m */, CB69C4EC25A3FD8A0030CFCD /* SCXPCAuthorization.h */, CB69C4ED25A3FD8A0030CFCD /* SCXPCAuthorization.m */, CB62FC3924B124B900ADBC40 /* SCXPCClient.h */, @@ -1345,7 +1338,6 @@ CB81A9D525B7C269006956F7 /* SCBlockUtilities.m in Sources */, CB62FC4424B1329800ADBC40 /* HostFileBlocker.m in Sources */, CB62FC3E24B1298500ADBC40 /* SCDaemonBlockMethods.m in Sources */, - CB62FC4124B1328F00ADBC40 /* HelperCommon.m in Sources */, CB81A94E25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, CB8086D424837607004B88BD /* SCDaemonXPC.m in Sources */, CB1CA65125ABA5BB0084A551 /* SCXPCClient.m in Sources */, @@ -1383,7 +1375,6 @@ CB9C812719CFBB6400CDCAE1 /* NSString+IPAddress.m in Sources */, CBE44FEB19E50900004E9706 /* AllowlistScraper.m in Sources */, CB9C812619CFBB5E00CDCAE1 /* BlockManager.m in Sources */, - CB9C812519CFBB5500CDCAE1 /* HelperCommon.m in Sources */, CB81AA3E25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */, CB9C812419CFBB4E00CDCAE1 /* HostFileBlocker.m in Sources */, CB81A94C25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, @@ -1416,7 +1407,6 @@ CBC2F8580F4672FE00CF2A42 /* LaunchctlHelper.m in Sources */, CBB0AE2A0FA74566006229B3 /* HostFileBlocker.m in Sources */, CB81A94A25B7B5B6006956F7 /* SCMigrationUtilities.m in Sources */, - CBD266B011ED7D9C00042CD8 /* HelperCommon.m in Sources */, CB32D2A921902CB300B8CD68 /* SCSettings.m in Sources */, CB1CA65025ABA5BB0084A551 /* SCXPCClient.m in Sources */, CB25806216C1FDBE0059C99A /* BlockManager.m in Sources */, From 2d710facd36580c18e70940e2f3baba7510574ac Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 19:37:41 -0800 Subject: [PATCH 59/72] Fix bug where block wouldn't get removed oops --- AppController.m | 2 ++ Common/Utility/SCHelperToolUtilities.m | 5 ++--- Daemon/SCDaemonBlockMethods.m | 2 +- Info.plist | 2 +- SelfControl Killer/Info.plist | 2 +- SelfControl.xcodeproj/project.pbxproj | 10 ++++++---- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/AppController.m b/AppController.m index 36b793e8..7ce2fca3 100755 --- a/AppController.m +++ b/AppController.m @@ -761,6 +761,7 @@ - (IBAction)save:(id)sender { } - (IBAction)open:(id)sender { + NSLog(@"CALLED OPEN OPENING FILE!"); NSOpenPanel* oPanel = [NSOpenPanel openPanel]; oPanel.allowedFileTypes = @[@"selfcontrol"]; oPanel.allowsMultipleSelection = NO; @@ -793,6 +794,7 @@ - (IBAction)open:(id)sender { } - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename { + NSLog(@"CALLED APPLICATION OPEN FILE!"); NSDictionary* openedDict = [NSDictionary dictionaryWithContentsOfFile: filename]; if(openedDict == nil) return NO; diff --git a/Common/Utility/SCHelperToolUtilities.m b/Common/Utility/SCHelperToolUtilities.m index 813ff714..1fee1f11 100644 --- a/Common/Utility/SCHelperToolUtilities.m +++ b/Common/Utility/SCHelperToolUtilities.m @@ -144,8 +144,7 @@ + (void)playBlockEndSound { } + (void)removeBlock { - SCSettings* settings = [SCSettings sharedSettings]; - + [SCBlockUtilities removeBlockFromSettings]; [[BlockManager new] clearBlock]; [SCHelperToolUtilities clearCachesIfRequested]; @@ -156,7 +155,7 @@ + (void)removeBlock { // always synchronize settings ASAP after removing a block to let everybody else know // and wait until they're synced before we send the configuration change notification // so the app has no chance of reading the data before we update it - NSError* syncErr = [settings syncSettingsAndWait: 5.0]; + NSError* syncErr = [[SCSettings sharedSettings] syncSettingsAndWait: 5.0]; if (syncErr != nil) { NSLog(@"WARNING: Sync failed or timed out with error %@ after removing block", syncErr); [SCSentry captureError: syncErr]; diff --git a/Daemon/SCDaemonBlockMethods.m b/Daemon/SCDaemonBlockMethods.m index e12e6c5a..95c3d42a 100644 --- a/Daemon/SCDaemonBlockMethods.m +++ b/Daemon/SCDaemonBlockMethods.m @@ -301,7 +301,7 @@ + (void)checkupBlock { [SCHelperToolUtilities sendConfigurationChangedNotification]; [SCSentry addBreadcrumb: @"Daemon found and cleared expired block" category: @"daemon"]; - + // once the checkups stop, the daemon will clear itself in a while due to inactivity [[SCDaemon sharedDaemon] stopCheckupTimer]; } else if ([[NSDate date] timeIntervalSinceDate: lastBlockIntegrityCheck] > integrityCheckIntervalSecs) { diff --git a/Info.plist b/Info.plist index 9ffa70bf..702bf8bf 100755 --- a/Info.plist +++ b/Info.plist @@ -58,7 +58,7 @@ SMPrivilegedExecutables org.eyebeam.selfcontrold - anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) + anchor apple generic and identifier "org.eyebeam.selfcontrold" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = L6W5L88KN7) SUEnableAutomaticChecks diff --git a/SelfControl Killer/Info.plist b/SelfControl Killer/Info.plist index 5ea7d07d..c2ddc2a8 100644 --- a/SelfControl Killer/Info.plist +++ b/SelfControl Killer/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.0.3 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index e64a8b51..5c511484 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -1845,7 +1845,7 @@ GCC_VERSION = ""; INFOPLIST_FILE = "$(SRCROOT)/Daemon/selfcontrold-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MARKETING_VERSION = 4.0; + MARKETING_VERSION = "4.0 alpha"; OTHER_LDFLAGS = ( "-sectcreate", __TEXT, @@ -1886,7 +1886,7 @@ GCC_VERSION = ""; INFOPLIST_FILE = "$(SRCROOT)/Daemon/selfcontrold-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MARKETING_VERSION = 4.0; + MARKETING_VERSION = "4.0 alpha"; OTHER_LDFLAGS = ( "-sectcreate", __TEXT, @@ -1948,6 +1948,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = "SelfControl Killer/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MARKETING_VERSION = "4.0 alpha"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.selfcontrolapp.SelfControl-Killer"; @@ -1988,6 +1989,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = "SelfControl Killer/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MARKETING_VERSION = "4.0 alpha"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.selfcontrolapp.SelfControl-Killer"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2097,7 +2099,7 @@ INFOPLIST_FILE = "$(SRCROOT)/selfcontrol-cli-Info.plist"; INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MARKETING_VERSION = 4.0; + MARKETING_VERSION = "4.0 alpha"; PRODUCT_BUNDLE_IDENTIFIER = "org.eyebeam.selfcontrol-cli"; PRODUCT_NAME = "selfcontrol-cli"; PROVISIONING_PROFILE = ""; @@ -2127,7 +2129,7 @@ INFOPLIST_FILE = "$(SRCROOT)/selfcontrol-cli-Info.plist"; INFOPLIST_PREPROCESS = YES; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; - MARKETING_VERSION = 4.0; + MARKETING_VERSION = "4.0 alpha"; PRODUCT_BUNDLE_IDENTIFIER = "org.eyebeam.selfcontrol-cli"; PRODUCT_NAME = "selfcontrol-cli"; PROVISIONING_PROFILE = ""; From 697be31b9f2351236af5eae5cb20e7bca734177a Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 20:16:26 -0800 Subject: [PATCH 60/72] Fix cocoapods warnings + minor refactor on file opening --- AppController.m | 72 +++++++++++---------------- Podfile | 28 ++++++++--- SelfControl.xcodeproj/project.pbxproj | 48 ++++++++---------- TimerWindowController.m | 16 +++--- 4 files changed, 78 insertions(+), 86 deletions(-) diff --git a/AppController.m b/AppController.m index 7ce2fca3..80bf74a5 100755 --- a/AppController.m +++ b/AppController.m @@ -738,7 +738,7 @@ - (IBAction)save:(id)sender { runResult = [sp runModal]; /* if successful, save file under designated name */ - if (runResult == NSOKButton) { + if (runResult == NSModalResponseOK) { NSString* errDescription; [SCBlockFileReaderWriter writeBlocklistToFileURL: sp.URL blockInfo: @{ @@ -760,60 +760,46 @@ - (IBAction)save:(id)sender { } } +- (BOOL)openSavedBlockFileAtURL:(NSURL*)fileURL { + NSDictionary* settingsFromFile = [SCBlockFileReaderWriter readBlocklistFromFile: fileURL]; + + if (settingsFromFile != nil) { + [defaults_ setObject: settingsFromFile[@"Blocklist"] forKey: @"Blocklist"]; + [defaults_ setObject: settingsFromFile[@"BlockAsWhitelist"] forKey: @"BlockAsWhitelist"]; + [SCSentry addBreadcrumb: @"Opened blocklist from file" category:@"app"]; + } else { + NSLog(@"WARNING: Could not read a valid blocklist from file - ignoring."); + return NO; + } + + // close the domain list (and reopen again if need be to refresh) + BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible]; + NSRect frame = [[domainListWindowController_ window] frame]; + [self closeDomainList]; + if(domainListIsOpen) { + [self showDomainList: self]; + [[domainListWindowController_ window] setFrame: frame display: YES]; + } + + [self refreshUserInterface]; + return YES; +} + - (IBAction)open:(id)sender { - NSLog(@"CALLED OPEN OPENING FILE!"); NSOpenPanel* oPanel = [NSOpenPanel openPanel]; oPanel.allowedFileTypes = @[@"selfcontrol"]; oPanel.allowsMultipleSelection = NO; long result = [oPanel runModal]; - if (result == NSOKButton) { + if (result == NSModalResponseOK) { if([oPanel.URLs count] > 0) { - NSDictionary* settingsFromFile = [SCBlockFileReaderWriter readBlocklistFromFile: oPanel.URLs[0]]; - - if (settingsFromFile != nil) { - [defaults_ setObject: settingsFromFile[@"Blocklist"] forKey: @"Blocklist"]; - [defaults_ setObject: settingsFromFile[@"BlockAsWhitelist"] forKey: @"BlockAsWhitelist"]; - [SCSentry addBreadcrumb: @"Opened blocklist from file" category:@"app"]; - } else { - NSLog(@"WARNING: Could not read a valid blocklist from file - ignoring."); - } - - // close the domain list (and reopen again if need be to refresh) - BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible]; - NSRect frame = [[domainListWindowController_ window] frame]; - [self closeDomainList]; - if(domainListIsOpen) { - [self showDomainList: self]; - [[domainListWindowController_ window] setFrame: frame display: YES]; - } - - [self refreshUserInterface]; + [self openSavedBlockFileAtURL: oPanel.URLs[0]]; } } } - (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename { - NSLog(@"CALLED APPLICATION OPEN FILE!"); - NSDictionary* openedDict = [NSDictionary dictionaryWithContentsOfFile: filename]; - if(openedDict == nil) return NO; - - NSArray* newBlocklist = openedDict[@"HostBlacklist"]; - NSNumber* newAllowlistChoice = openedDict[@"BlockAsWhitelist"]; - if(newBlocklist == nil || newAllowlistChoice == nil) return NO; - - [defaults_ setValue: newBlocklist forKey:@"Blocklist"]; - [defaults_ setObject: newAllowlistChoice forKey: @"BlockAsWhitelist"]; - - BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible]; - NSRect frame = [[domainListWindowController_ window] frame]; - [self closeDomainList]; - if(domainListIsOpen) { - [self showDomainList: self]; - [[domainListWindowController_ window] setFrame: frame display: YES]; - } - - return YES; + return [self openSavedBlockFileAtURL: [NSURL fileURLWithPath: filename]]; } - (IBAction)openFAQ:(id)sender { diff --git a/Podfile b/Podfile index 7a3b002d..f24e92a8 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,8 @@ source 'https://github.com/CocoaPods/Specs.git' -platform :osx, '10.10' + +minVersion = '10.10' + +platform :osx, minVersion # cocoapods-prune-localizations doesn't appear to auto-detect pods properly, so using a manual list supported_locales = ['Base', 'da', 'de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'nl', 'pt-BR', 'sv', 'tr', 'zh-Hans'] @@ -9,24 +12,35 @@ target "SelfControl" do use_frameworks! :linkage => :static pod 'MASPreferences', '~> 1.1.4' pod 'FormatterKit/TimeIntervalFormatter', '~> 1.8.0' - pod 'Sparkle', '~> 1.22' + pod 'Sparkle', :git => 'https://github.com/sparkle-project/Sparkle', :tag => '1.24.0' pod 'LetsMove', '~> 1.24' - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.2' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.3' end target "SelfControl Killer" do use_frameworks! :linkage => :static - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.2' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.3' end # we can't use_frameworks on these because they're command-line tools # Sentry says we need use_frameworks, but they seem to work OK anyway? target "SCKillerHelper" do - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.2' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.3' end target "selfcontrol-cli" do - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.2' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.3' end target "org.eyebeam.selfcontrold" do - pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.2' + pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.3' +end + +post_install do |pi| + pi.pods_project.targets.each do |t| + t.build_configurations.each do |bc| + if Gem::Version.new(bc.build_settings['MACOSX_DEPLOYMENT_TARGET']) < Gem::Version.new(minVersion) +# if bc.build_settings['MACOSX_DEPLOYMENT_TARGET'] == '8.0' + bc.build_settings['MACOSX_DEPLOYMENT_TARGET'] = minVersion + end + end + end end diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 5c511484..4dcd8a30 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -536,7 +536,7 @@ name = Products; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA /* SelfControl */ = { + 29B97314FDCFA39411CA2CEA = { isa = PBXGroup; children = ( CBDFFF4B24A07DB300622CEE /* SelfControl.entitlements */, @@ -815,8 +815,8 @@ isa = PBXNativeTarget; buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "SelfControl" */; buildPhases = ( - CB81A92E25B7B4B8006956F7 /* ShellScript */, 10CA088CE7CF641F4F172445 /* [CP] Check Pods Manifest.lock */, + CB81A92E25B7B4B8006956F7 /* ShellScript */, 8D1107290486CEB800E47090 /* Resources */, 8D11072C0486CEB800E47090 /* Sources */, 8D11072E0486CEB800E47090 /* Frameworks */, @@ -881,7 +881,7 @@ CB9C80F419CFB79700CDCAE1 /* Frameworks */, CB9C80F519CFB79700CDCAE1 /* Resources */, CB9C813119CFBBD300CDCAE1 /* Copy Helper Tools */, - 0708820BA3CCE4375AAC4438 /* [CP] Embed Pods Frameworks */, + 4908B8EBDC1C6FDD38B8BC40 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -992,7 +992,7 @@ Base, da, ); - mainGroup = 29B97314FDCFA39411CA2CEA /* SelfControl */; + mainGroup = 29B97314FDCFA39411CA2CEA; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1054,24 +1054,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0708820BA3CCE4375AAC4438 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SelfControl Killer/Pods-SelfControl Killer-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Sentry-framework/Sentry.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SelfControl Killer/Pods-SelfControl Killer-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 10CA088CE7CF641F4F172445 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1134,6 +1116,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 4908B8EBDC1C6FDD38B8BC40 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SelfControl Killer/Pods-SelfControl Killer-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Sentry-framework/Sentry.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SelfControl Killer/Pods-SelfControl Killer-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 7D55904F6A3D627B837007A9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1145,8 +1145,6 @@ "${BUILT_PRODUCTS_DIR}/LetsMove/LetsMove.framework", "${BUILT_PRODUCTS_DIR}/MASPreferences/MASPreferences.framework", "${BUILT_PRODUCTS_DIR}/Sentry-framework/Sentry.framework", - "${PODS_ROOT}/Sparkle/Sparkle.framework", - "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -1154,8 +1152,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LetsMove.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MASPreferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework", - "${DWARF_DSYM_FOLDER_PATH}/Sparkle.framework.dSYM", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/TimerWindowController.m b/TimerWindowController.m index dd3bf63b..9d286d3c 100755 --- a/TimerWindowController.m +++ b/TimerWindowController.m @@ -234,11 +234,9 @@ - (IBAction) addToBlock:(id)sender { return; } - [NSApp beginSheet: addSheet_ - modalForWindow: [self window] - modalDelegate: self - didEndSelector: @selector(didEndSheet:returnCode:contextInfo:) - contextInfo: nil]; + [self.window beginSheet: addSheet_ completionHandler:^(NSModalResponse returnCode) { + [self->addSheet_ orderOut: self]; + }]; [modifyBlockLock unlock]; } @@ -250,11 +248,9 @@ - (IBAction) extendBlockTime:(id)sender { return; } - [NSApp beginSheet: extendBlockTimeSheet_ - modalForWindow: [self window] - modalDelegate: self - didEndSelector: @selector(didEndSheet:returnCode:contextInfo:) - contextInfo: nil]; + [self.window beginSheet: extendBlockTimeSheet_ completionHandler:^(NSModalResponse returnCode) { + [self->extendBlockTimeSheet_ orderOut: self]; + }]; [modifyBlockLock unlock]; } From 1d153f60fd144017de8469ecb2f1c8dd77427bd3 Mon Sep 17 00:00:00 2001 From: Charlie Stigler Date: Tue, 19 Jan 2021 21:16:35 -0800 Subject: [PATCH 61/72] Commit Sparkle directly because CocoaPods is just terrible --- AppController.m | 18 +- Common/DeprecationSilencers.h | 23 + Common/SCBlockFileReaderWriter.h | 2 +- Common/SCBlockFileReaderWriter.m | 11 +- Common/Utility/SCHelperToolUtilities.m | 2 + DomainListWindowController.m | 17 +- Podfile | 1 - SCError.strings | 1 + SCKillerHelper/main.m | 2 + SelfControl.xcodeproj/project.pbxproj | 29 +- SelfControl_Prefix.pch | 1 + Sparkle.framework/Headers | 1 + Sparkle.framework/Modules | 1 + Sparkle.framework/PrivateHeaders | 1 + Sparkle.framework/Resources | 1 + Sparkle.framework/Sparkle | 1 + .../Versions/A/Headers/SPUDownloadData.h | 43 + .../Versions/A/Headers/SPUDownloader.h | 25 + .../A/Headers/SPUDownloaderDelegate.h | 38 + .../A/Headers/SPUDownloaderDeprecated.h | 13 + .../A/Headers/SPUDownloaderProtocol.h | 34 + .../Versions/A/Headers/SPUDownloaderSession.h | 20 + .../Versions/A/Headers/SPUURLRequest.h | 35 + .../Versions/A/Headers/SUAppcast.h | 35 + .../Versions/A/Headers/SUAppcastItem.h | 54 + .../A/Headers/SUCodeSigningVerifier.h | 26 + .../Versions/A/Headers/SUErrors.h | 57 + .../Versions/A/Headers/SUExport.h | 18 + .../A/Headers/SUStandardVersionComparator.h | 52 + .../Versions/A/Headers/SUUpdater.h | 233 + .../Versions/A/Headers/SUUpdaterDelegate.h | 352 ++ .../A/Headers/SUVersionComparisonProtocol.h | 37 + .../A/Headers/SUVersionDisplayProtocol.h | 29 + .../Versions/A/Headers/Sparkle.h | 39 + .../Versions/A/Modules/module.modulemap | 6 + .../Versions/A/PrivateHeaders/SUUnarchiver.h | 21 + .../Autoupdate.app/Contents/Info.plist | 56 + .../Autoupdate.app/Contents/MacOS/Autoupdate | Bin 0 -> 577184 bytes .../Autoupdate.app/Contents/MacOS/fileop | Bin 0 -> 284960 bytes .../Resources/Autoupdate.app/Contents/PkgInfo | 1 + .../Contents/Resources/AppIcon.icns | Bin 0 -> 37132 bytes .../Contents/Resources/SUStatus.nib | Bin 0 -> 12667 bytes .../Resources/ar.lproj/Sparkle.strings | Bin 0 -> 8468 bytes .../Resources/ca.lproj/Sparkle.strings | Bin 0 -> 6792 bytes .../Resources/cs.lproj/Sparkle.strings | Bin 0 -> 10638 bytes .../Resources/da.lproj/Sparkle.strings | Bin 0 -> 8306 bytes .../Resources/de.lproj/Sparkle.strings | Bin 0 -> 10162 bytes .../Resources/el.lproj/Sparkle.strings | Bin 0 -> 7734 bytes .../Resources/en.lproj/Sparkle.strings | Bin 0 -> 10196 bytes .../Resources/es.lproj/Sparkle.strings | Bin 0 -> 10030 bytes .../Resources/fi.lproj/Sparkle.strings | Bin 0 -> 6184 bytes .../Resources/fr.lproj/Sparkle.strings | Bin 0 -> 10170 bytes .../Resources/he.lproj/Sparkle.strings | Bin 0 -> 5288 bytes .../Resources/hr.lproj/Sparkle.strings | Bin 0 -> 9778 bytes .../Resources/hu.lproj/Sparkle.strings | Bin 0 -> 10002 bytes .../Resources/is.lproj/Sparkle.strings | Bin 0 -> 5868 bytes .../Resources/it.lproj/Sparkle.strings | Bin 0 -> 8340 bytes .../Resources/ja.lproj/Sparkle.strings | Bin 0 -> 8552 bytes .../Resources/ko.lproj/Sparkle.strings | Bin 0 -> 6220 bytes .../Resources/nb.lproj/Sparkle.strings | Bin 0 -> 9352 bytes .../Resources/nl.lproj/Sparkle.strings | Bin 0 -> 8850 bytes .../Resources/pl.lproj/Sparkle.strings | Bin 0 -> 7318 bytes .../Resources/pt_BR.lproj/Sparkle.strings | Bin 0 -> 9728 bytes .../Resources/pt_PT.lproj/Sparkle.strings | Bin 0 -> 7896 bytes .../Resources/ro.lproj/Sparkle.strings | Bin 0 -> 9806 bytes .../Resources/ru.lproj/Sparkle.strings | Bin 0 -> 7856 bytes .../Resources/sk.lproj/Sparkle.strings | Bin 0 -> 7548 bytes .../Resources/sl.lproj/Sparkle.strings | Bin 0 -> 7944 bytes .../Resources/sv.lproj/Sparkle.strings | Bin 0 -> 7834 bytes .../Resources/th.lproj/Sparkle.strings | Bin 0 -> 8492 bytes .../Resources/tr.lproj/Sparkle.strings | Bin 0 -> 9716 bytes .../Resources/uk.lproj/Sparkle.strings | Bin 0 -> 7860 bytes .../Resources/zh_CN.lproj/Sparkle.strings | Bin 0 -> 6128 bytes .../Resources/zh_TW.lproj/Sparkle.strings | Bin 0 -> 5748 bytes .../Contents/_CodeSignature/CodeResources | 860 ++++ .../Versions/A/Resources/DarkAqua.css | 9 + .../Versions/A/Resources/Info.plist | 48 + .../A/Resources/SUModelTranslation.plist | 314 ++ .../Versions/A/Resources/SUStatus.nib | Bin 0 -> 12667 bytes .../ar.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13606 bytes .../A/Resources/ar.lproj/SUUpdateAlert.nib | Bin 0 -> 23198 bytes .../ar.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20579 bytes .../A/Resources/ar.lproj/Sparkle.strings | Bin 0 -> 8468 bytes .../A/Resources/ca.lproj/Sparkle.strings | Bin 0 -> 6792 bytes .../cs.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13594 bytes .../A/Resources/cs.lproj/SUUpdateAlert.nib | Bin 0 -> 23224 bytes .../cs.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20677 bytes .../A/Resources/cs.lproj/Sparkle.strings | Bin 0 -> 10638 bytes .../da.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13503 bytes .../A/Resources/da.lproj/SUUpdateAlert.nib | Bin 0 -> 23103 bytes .../da.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20466 bytes .../A/Resources/da.lproj/Sparkle.strings | Bin 0 -> 8306 bytes .../de.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13522 bytes .../A/Resources/de.lproj/SUUpdateAlert.nib | Bin 0 -> 23140 bytes .../de.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20619 bytes .../A/Resources/de.lproj/Sparkle.strings | Bin 0 -> 10162 bytes .../el.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13646 bytes .../A/Resources/el.lproj/SUUpdateAlert.nib | Bin 0 -> 23239 bytes .../el.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20709 bytes .../A/Resources/el.lproj/Sparkle.strings | Bin 0 -> 7734 bytes .../en.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13540 bytes .../A/Resources/en.lproj/SUUpdateAlert.nib | Bin 0 -> 23132 bytes .../en.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20408 bytes .../A/Resources/en.lproj/Sparkle.strings | Bin 0 -> 10196 bytes .../es.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13556 bytes .../A/Resources/es.lproj/SUUpdateAlert.nib | Bin 0 -> 23274 bytes .../es.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20795 bytes .../A/Resources/es.lproj/Sparkle.strings | Bin 0 -> 10030 bytes .../fi.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13646 bytes .../A/Resources/fi.lproj/SUUpdateAlert.nib | Bin 0 -> 23247 bytes .../fi.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20517 bytes .../A/Resources/fi.lproj/Sparkle.strings | Bin 0 -> 6184 bytes .../fr.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13559 bytes .../A/Resources/fr.lproj/SUUpdateAlert.nib | Bin 0 -> 23196 bytes .../fr.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20787 bytes .../A/Resources/fr.lproj/Sparkle.strings | Bin 0 -> 10170 bytes .../Versions/A/Resources/fr_CA.lproj | 1 + .../A/Resources/he.lproj/Sparkle.strings | Bin 0 -> 5288 bytes .../hr.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13607 bytes .../A/Resources/hr.lproj/SUUpdateAlert.nib | Bin 0 -> 23222 bytes .../hr.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20618 bytes .../A/Resources/hr.lproj/Sparkle.strings | Bin 0 -> 9778 bytes .../hu.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13648 bytes .../A/Resources/hu.lproj/SUUpdateAlert.nib | Bin 0 -> 23302 bytes .../hu.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20517 bytes .../A/Resources/hu.lproj/Sparkle.strings | Bin 0 -> 10002 bytes .../is.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13570 bytes .../A/Resources/is.lproj/SUUpdateAlert.nib | Bin 0 -> 23209 bytes .../is.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20707 bytes .../A/Resources/is.lproj/Sparkle.strings | Bin 0 -> 5868 bytes .../it.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13506 bytes .../A/Resources/it.lproj/SUUpdateAlert.nib | Bin 0 -> 23134 bytes .../it.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20644 bytes .../A/Resources/it.lproj/Sparkle.strings | Bin 0 -> 8340 bytes .../ja.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13510 bytes .../A/Resources/ja.lproj/SUUpdateAlert.nib | Bin 0 -> 23114 bytes .../ja.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20337 bytes .../A/Resources/ja.lproj/Sparkle.strings | Bin 0 -> 8552 bytes .../ko.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13478 bytes .../A/Resources/ko.lproj/SUUpdateAlert.nib | Bin 0 -> 23068 bytes .../ko.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20344 bytes .../A/Resources/ko.lproj/Sparkle.strings | Bin 0 -> 6220 bytes .../nb.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13522 bytes .../A/Resources/nb.lproj/SUUpdateAlert.nib | Bin 0 -> 23063 bytes .../nb.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20600 bytes .../A/Resources/nb.lproj/Sparkle.strings | Bin 0 -> 9352 bytes .../nl.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13518 bytes .../A/Resources/nl.lproj/SUUpdateAlert.nib | Bin 0 -> 23107 bytes .../nl.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20424 bytes .../A/Resources/nl.lproj/Sparkle.strings | Bin 0 -> 8850 bytes .../pl.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13597 bytes .../A/Resources/pl.lproj/SUUpdateAlert.nib | Bin 0 -> 23226 bytes .../pl.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20491 bytes .../A/Resources/pl.lproj/Sparkle.strings | Bin 0 -> 7318 bytes .../Versions/A/Resources/pt.lproj | 1 + .../pt_BR.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13566 bytes .../A/Resources/pt_BR.lproj/SUUpdateAlert.nib | Bin 0 -> 23247 bytes .../pt_BR.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20719 bytes .../A/Resources/pt_BR.lproj/Sparkle.strings | Bin 0 -> 9728 bytes .../pt_PT.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13580 bytes .../A/Resources/pt_PT.lproj/SUUpdateAlert.nib | Bin 0 -> 23274 bytes .../pt_PT.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20759 bytes .../A/Resources/pt_PT.lproj/Sparkle.strings | Bin 0 -> 7896 bytes .../ro.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13608 bytes .../A/Resources/ro.lproj/SUUpdateAlert.nib | Bin 0 -> 23258 bytes .../ro.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20477 bytes .../A/Resources/ro.lproj/Sparkle.strings | Bin 0 -> 9806 bytes .../ru.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13648 bytes .../A/Resources/ru.lproj/SUUpdateAlert.nib | Bin 0 -> 23308 bytes .../ru.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20793 bytes .../A/Resources/ru.lproj/Sparkle.strings | Bin 0 -> 7856 bytes .../sk.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13626 bytes .../A/Resources/sk.lproj/SUUpdateAlert.nib | Bin 0 -> 23260 bytes .../sk.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20705 bytes .../A/Resources/sk.lproj/Sparkle.strings | Bin 0 -> 7548 bytes .../sl.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13536 bytes .../A/Resources/sl.lproj/SUUpdateAlert.nib | Bin 0 -> 23172 bytes .../sl.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20624 bytes .../A/Resources/sl.lproj/Sparkle.strings | Bin 0 -> 7944 bytes .../sv.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13585 bytes .../A/Resources/sv.lproj/SUUpdateAlert.nib | Bin 0 -> 23234 bytes .../sv.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20620 bytes .../A/Resources/sv.lproj/Sparkle.strings | Bin 0 -> 7834 bytes .../th.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13572 bytes .../A/Resources/th.lproj/SUUpdateAlert.nib | Bin 0 -> 23177 bytes .../th.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20539 bytes .../A/Resources/th.lproj/Sparkle.strings | Bin 0 -> 8492 bytes .../tr.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13610 bytes .../A/Resources/tr.lproj/SUUpdateAlert.nib | Bin 0 -> 23178 bytes .../tr.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20688 bytes .../A/Resources/tr.lproj/Sparkle.strings | Bin 0 -> 9716 bytes .../uk.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13666 bytes .../A/Resources/uk.lproj/SUUpdateAlert.nib | Bin 0 -> 23328 bytes .../uk.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20702 bytes .../A/Resources/uk.lproj/Sparkle.strings | Bin 0 -> 7860 bytes .../zh_CN.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13444 bytes .../A/Resources/zh_CN.lproj/SUUpdateAlert.nib | Bin 0 -> 23012 bytes .../zh_CN.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20263 bytes .../A/Resources/zh_CN.lproj/Sparkle.strings | Bin 0 -> 6128 bytes .../zh_TW.lproj/SUAutomaticUpdateAlert.nib | Bin 0 -> 13450 bytes .../A/Resources/zh_TW.lproj/SUUpdateAlert.nib | Bin 0 -> 23029 bytes .../zh_TW.lproj/SUUpdatePermissionPrompt.nib | Bin 0 -> 20273 bytes .../A/Resources/zh_TW.lproj/Sparkle.strings | Bin 0 -> 5748 bytes Sparkle.framework/Versions/A/Sparkle | Bin 0 -> 1418416 bytes .../Versions/A/_CodeSignature/CodeResources | 3900 +++++++++++++++++ Sparkle.framework/Versions/Current | 1 + cli-main.m | 4 - distribution-build.rb | 8 +- 208 files changed, 6444 insertions(+), 38 deletions(-) create mode 100644 Common/DeprecationSilencers.h create mode 120000 Sparkle.framework/Headers create mode 120000 Sparkle.framework/Modules create mode 120000 Sparkle.framework/PrivateHeaders create mode 120000 Sparkle.framework/Resources create mode 120000 Sparkle.framework/Sparkle create mode 100644 Sparkle.framework/Versions/A/Headers/SPUDownloadData.h create mode 100644 Sparkle.framework/Versions/A/Headers/SPUDownloader.h create mode 100644 Sparkle.framework/Versions/A/Headers/SPUDownloaderDelegate.h create mode 100644 Sparkle.framework/Versions/A/Headers/SPUDownloaderDeprecated.h create mode 100644 Sparkle.framework/Versions/A/Headers/SPUDownloaderProtocol.h create mode 100644 Sparkle.framework/Versions/A/Headers/SPUDownloaderSession.h create mode 100644 Sparkle.framework/Versions/A/Headers/SPUURLRequest.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUAppcast.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUAppcastItem.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUCodeSigningVerifier.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUErrors.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUExport.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUUpdater.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h create mode 100644 Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h create mode 100644 Sparkle.framework/Versions/A/Headers/Sparkle.h create mode 100644 Sparkle.framework/Versions/A/Modules/module.modulemap create mode 100644 Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist create mode 100755 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate create mode 100755 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/fileop create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ca.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fi.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/he.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/hr.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/hu.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/_CodeSignature/CodeResources create mode 100644 Sparkle.framework/Versions/A/Resources/DarkAqua.css create mode 100644 Sparkle.framework/Versions/A/Resources/Info.plist create mode 100644 Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist create mode 100644 Sparkle.framework/Versions/A/Resources/SUStatus.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ar.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ar.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/ca.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/cs.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/cs.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/da.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/da.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/de.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/el.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/el.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/es.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/fi.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/fi.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/fi.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/fi.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/fr.lproj/Sparkle.strings create mode 120000 Sparkle.framework/Versions/A/Resources/fr_CA.lproj create mode 100644 Sparkle.framework/Versions/A/Resources/he.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/hr.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/hr.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/hr.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/hr.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/hu.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/hu.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/hu.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/hu.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/is.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/is.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/it.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/ja.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ja.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/ko.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ko.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/nb.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/nb.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/nl.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/pl.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pl.lproj/Sparkle.strings create mode 120000 Sparkle.framework/Versions/A/Resources/pt.lproj create mode 100644 Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pt_BR.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/pt_PT.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/ro.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ro.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/ru.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/sk.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sk.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/sl.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sl.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/sv.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/th.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/th.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/tr.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/tr.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/uk.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/uk.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/zh_CN.lproj/Sparkle.strings create mode 100644 Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUAutomaticUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdateAlert.nib create mode 100644 Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdatePermissionPrompt.nib create mode 100644 Sparkle.framework/Versions/A/Resources/zh_TW.lproj/Sparkle.strings create mode 100755 Sparkle.framework/Versions/A/Sparkle create mode 100644 Sparkle.framework/Versions/A/_CodeSignature/CodeResources create mode 120000 Sparkle.framework/Versions/Current diff --git a/AppController.m b/AppController.m index 80bf74a5..f868808f 100755 --- a/AppController.m +++ b/AppController.m @@ -84,12 +84,12 @@ - (IBAction)updateTimeSliderDisplay:(id)sender { - (NSString *)timeSliderDisplayStringFromNumberOfMinutes:(NSInteger)numberOfMinutes { static NSCalendar* gregorian = nil; if (gregorian == nil) { - gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; + gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; } NSRange secondsRangePerMinute = [gregorian - rangeOfUnit:NSSecondCalendarUnit - inUnit:NSMinuteCalendarUnit + rangeOfUnit:NSCalendarUnitSecond + inUnit:NSCalendarUnitMinute forDate:[NSDate date]]; NSUInteger numberOfSecondsPerMinute = NSMaxRange(secondsRangePerMinute); @@ -255,7 +255,7 @@ - (void)handleConfigurationChangedNotification { - (void)showTimerWindow { if(timerWindowController_ == nil) { - [NSBundle loadNibNamed: @"TimerWindow" owner: self]; + [[NSBundle mainBundle] loadNibNamed: @"TimerWindow" owner: self topLevelObjects: nil]; } else { [[timerWindowController_ window] makeKeyAndOrderFront: self]; [[timerWindowController_ window] center]; @@ -433,7 +433,7 @@ - (IBAction)showDomainList:(id)sender { } if(domainListWindowController_ == nil) { - [NSBundle loadNibNamed: @"DomainList" owner: self]; + [[NSBundle mainBundle] loadNibNamed: @"DomainList" owner: self topLevelObjects: nil]; } [domainListWindowController_ showWindow: self]; } @@ -739,17 +739,17 @@ - (IBAction)save:(id)sender { /* if successful, save file under designated name */ if (runResult == NSModalResponseOK) { - NSString* errDescription; + NSError* err; [SCBlockFileReaderWriter writeBlocklistToFileURL: sp.URL blockInfo: @{ @"Blocklist": [defaults_ arrayForKey: @"Blocklist"], @"BlockAsWhitelist": [defaults_ objectForKey: @"BlockAsWhitelist"] } - errorDescription: &errDescription]; + error: &err]; - if(errDescription) { - NSError* displayErr = [SCErr errorWithCode: 105 subDescription: errDescription]; + if (err != nil) { + NSError* displayErr = [SCErr errorWithCode: 105 subDescription: err.localizedDescription]; [SCSentry captureError: displayErr]; NSBeep(); [NSApp presentError: displayErr]; diff --git a/Common/DeprecationSilencers.h b/Common/DeprecationSilencers.h new file mode 100644 index 00000000..949b4df7 --- /dev/null +++ b/Common/DeprecationSilencers.h @@ -0,0 +1,23 @@ +// +// DeprecationSilencers.h +// SelfControl +// +// Created by Charlie Stigler on 1/19/21. +// + +#ifndef DeprecationSilencers_h +#define DeprecationSilencers_h + +// great idea taken from this guy: https://stackoverflow.com/a/26564750 + +#define SILENCE_DEPRECATION(expr) \ +do { \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") \ +expr; \ +_Pragma("clang diagnostic pop") \ +} while(0) + +#define SILENCE_OSX10_10_DEPRECATION(expr) SILENCE_DEPRECATION(expr) + +#endif /* DeprecationSilencers_h */ diff --git a/Common/SCBlockFileReaderWriter.h b/Common/SCBlockFileReaderWriter.h index 55c06626..742c5345 100644 --- a/Common/SCBlockFileReaderWriter.h +++ b/Common/SCBlockFileReaderWriter.h @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN // Writes out a saved .selfcontrol blocklist file to the file system // containing the block info (blocklist + whitelist setting) defined // in blockInfo. -+ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef; ++ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo error:(NSError*_Nullable*_Nullable)errRef; // reads in a saved .selfcontrol blocklist file and returns // an NSDictionary with the block settings contained diff --git a/Common/SCBlockFileReaderWriter.m b/Common/SCBlockFileReaderWriter.m index 55a6fa7e..cdcd8e27 100644 --- a/Common/SCBlockFileReaderWriter.m +++ b/Common/SCBlockFileReaderWriter.m @@ -9,25 +9,24 @@ @implementation SCBlockFileReaderWriter -+ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo errorDescription:(NSString**)errDescriptionRef { ++ (BOOL)writeBlocklistToFileURL:(NSURL*)targetFileURL blockInfo:(NSDictionary*)blockInfo error:(NSError**)errRef { NSDictionary* saveDict = @{@"HostBlacklist": [blockInfo objectForKey: @"Blocklist"], @"BlockAsWhitelist": [blockInfo objectForKey: @"BlockAsWhitelist"]}; - NSString* saveDataErr; - NSData* saveData = [NSPropertyListSerialization dataFromPropertyList: saveDict format: NSPropertyListBinaryFormat_v1_0 errorDescription: &saveDataErr]; - if (saveDataErr != nil) { - *errDescriptionRef = saveDataErr; + NSData* saveData = [NSPropertyListSerialization dataWithPropertyList: saveDict format: NSPropertyListBinaryFormat_v1_0 options: 0 error: errRef]; + if (*errRef != nil) { return NO; } if (![saveData writeToURL: targetFileURL atomically: YES]) { NSLog(@"ERROR: Failed to write blocklist to URL %@", targetFileURL); + *errRef = [SCErr errorWithCode: 106]; return NO; } // for prettiness sake, attempt to hide the file extension NSDictionary* attribs = @{NSFileExtensionHidden: @YES}; - [[NSFileManager defaultManager] setAttributes: attribs ofItemAtPath: [targetFileURL path] error: NULL]; + [[NSFileManager defaultManager] setAttributes: attribs ofItemAtPath: [targetFileURL path] error: errRef]; return YES; } diff --git a/Common/Utility/SCHelperToolUtilities.m b/Common/Utility/SCHelperToolUtilities.m index 1fee1f11..344a9f10 100644 --- a/Common/Utility/SCHelperToolUtilities.m +++ b/Common/Utility/SCHelperToolUtilities.m @@ -47,7 +47,9 @@ + (void)unloadDaemonJob { // uh-oh, looks like it's 5 seconds later and the sync hasn't completed yet. Bad news. CFErrorRef cfError; // this should block until the process is dead, so we should never get to the other side if it's successful + SILENCE_OSX10_10_DEPRECATION( SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, YES, &cfError); + ); if (cfError) { NSLog(@"Failed to remove selfcontrold daemon with error %@", cfError); } diff --git a/DomainListWindowController.m b/DomainListWindowController.m index d5e494ea..d8bdc47c 100755 --- a/DomainListWindowController.m +++ b/DomainListWindowController.m @@ -247,13 +247,16 @@ - (IBAction)allowlistOptionChanged:(NSMatrix*)sender { } - (void)showAllowlistWarning { - if(![defaults_ boolForKey: @"WhitelistAlertSuppress"]) { - NSAlert* a = [NSAlert alertWithMessageText: NSLocalizedString(@"Are you sure you want an allowlist block?", @"Allowlist block confirmation prompt") defaultButton: NSLocalizedString(@"OK", @"OK button") alternateButton: @"" otherButton: @"" informativeTextWithFormat: NSLocalizedString(@"An allowlist block means that everything on the internet BESIDES your specified list will be blocked. This includes the web, email, SSH, and anything else your computer accesses via the internet. If a web site requires resources such as images or scripts from a site that is not on your allowlist, the site may not work properly.", @"allowlist block explanation")]; - if([a respondsToSelector: @selector(setShowsSuppressionButton:)]) { - [a setShowsSuppressionButton: YES]; - } - [a runModal]; - if([a respondsToSelector: @selector(suppressionButton)] && [[a suppressionButton] state] == NSOnState) { + if(![defaults_ boolForKey: @"WhitelistAlertSuppress"]) { + NSAlert* alert = [NSAlert new]; + alert.messageText = NSLocalizedString(@"Are you sure you want an allowlist block?", @"Allowlist block confirmation prompt"); + alert.buttons[0].title = NSLocalizedString(@"OK", @"OK button"); + alert.informativeText = NSLocalizedString(@"An allowlist block means that everything on the internet BESIDES your specified list will be blocked. This includes the web, email, SSH, and anything else your computer accesses via the internet. If a web site requires resources such as images or scripts from a site that is not on your allowlist, the site may not work properly.", @"allowlist block explanation"); + alert.showsSuppressionButton = YES; + + [alert runModal]; + + if (alert.suppressionButton.state == NSOnState) { [defaults_ setBool: YES forKey: @"WhitelistAlertSuppress"]; } } diff --git a/Podfile b/Podfile index f24e92a8..d62d5e72 100644 --- a/Podfile +++ b/Podfile @@ -12,7 +12,6 @@ target "SelfControl" do use_frameworks! :linkage => :static pod 'MASPreferences', '~> 1.1.4' pod 'FormatterKit/TimeIntervalFormatter', '~> 1.8.0' - pod 'Sparkle', :git => 'https://github.com/sparkle-project/Sparkle', :tag => '1.24.0' pod 'LetsMove', '~> 1.24' pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '6.1.3' end diff --git a/SCError.strings b/SCError.strings index afb41863..48fcc012 100644 --- a/SCError.strings +++ b/SCError.strings @@ -16,6 +16,7 @@ "103" = "You can't extend block duration, because no block is currently running."; "104" = "You can't start a block, because another block is currently running."; "105" = "The block wasn't removed at the scheduled time, for unknown reasons."; +"106" = "Data couldn't be written to that location."; // 200 - 299 = errors generated in the CLI diff --git a/SCKillerHelper/main.m b/SCKillerHelper/main.m index 4741408a..e198586c 100644 --- a/SCKillerHelper/main.m +++ b/SCKillerHelper/main.m @@ -137,7 +137,9 @@ int main(int argc, char* argv[]) { [log appendFormat: @"Unloading the legacy (1.0 - 3.0.3) launchd daemon returned: %d\n\n", status]; CFErrorRef cfError; + SILENCE_OSX10_10_DEPRECATION( SMJobRemove(kSMDomainSystemLaunchd, CFSTR("org.eyebeam.selfcontrold"), NULL, YES, &cfError); + ); if (cfError) { [log appendFormat: @"Failed to remove selfcontrold daemon (4.x) with error %@\n\n", cfError]; } else { diff --git a/SelfControl.xcodeproj/project.pbxproj b/SelfControl.xcodeproj/project.pbxproj index 4dcd8a30..83c1f8b1 100644 --- a/SelfControl.xcodeproj/project.pbxproj +++ b/SelfControl.xcodeproj/project.pbxproj @@ -103,6 +103,9 @@ CB81AA3E25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; CB81AA3F25B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; CB81AA4025B7D152006956F7 /* SCHelperToolUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */; }; + CB81AAB725B7E6C7006956F7 /* DeprecationSilencers.h in Headers */ = {isa = PBXBuildFile; fileRef = CB81AAB625B7E6C7006956F7 /* DeprecationSilencers.h */; }; + CB81AB2525B7EFA4006956F7 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB81AB2325B7EF74006956F7 /* Sparkle.framework */; }; + CB81AB2625B7EFA4006956F7 /* Sparkle.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CB81AB2325B7EF74006956F7 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CB850F3925130F5300EE2E2D /* NSString+IPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = CB25806516C237F10059C99A /* NSString+IPAddress.m */; }; CB90BF830F49F430006D202D /* HostImporter.m in Sources */ = {isa = PBXBuildFile; fileRef = CB90BF820F49F430006D202D /* HostImporter.m */; }; CB9365620F8581B000EF284E /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CB9365610F8581B000EF284E /* dsa_pub.pem */; }; @@ -188,6 +191,17 @@ name = "Copy Executable Helper Tools"; runOnlyForDeploymentPostprocessing = 0; }; + CB81AB2725B7EFA4006956F7 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CB81AB2625B7EFA4006956F7 /* Sparkle.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; CB9C813119CFBBD300CDCAE1 /* Copy Helper Tools */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -293,6 +307,8 @@ CB81A9F125B7C5F7006956F7 /* SCBlockFileReaderWriter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCBlockFileReaderWriter.m; sourceTree = ""; }; CB81AA3825B7D152006956F7 /* SCHelperToolUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SCHelperToolUtilities.h; sourceTree = ""; }; CB81AA3925B7D152006956F7 /* SCHelperToolUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SCHelperToolUtilities.m; sourceTree = ""; }; + CB81AAB625B7E6C7006956F7 /* DeprecationSilencers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DeprecationSilencers.h; sourceTree = ""; }; + CB81AB2325B7EF74006956F7 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sparkle.framework; sourceTree = ""; }; CB90BF810F49F430006D202D /* HostImporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HostImporter.h; sourceTree = ""; }; CB90BF820F49F430006D202D /* HostImporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HostImporter.m; sourceTree = ""; }; CB9365610F8581B000EF284E /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dsa_pub.pem; sourceTree = ""; }; @@ -340,7 +356,6 @@ CBCA91111960D87300AFD20C /* PacketFilter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PacketFilter.m; sourceTree = ""; }; CBCA91271961381F00AFD20C /* tr */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CBCA912B1961384600AFD20C /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; - CBD4848219D75F6D0020F949 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sparkle.framework; sourceTree = ""; }; CBD4848519D7611F0020F949 /* Podfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; CBD4848619D7611F0020F949 /* TemplateIcon2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TemplateIcon2x.png; sourceTree = ""; }; CBD4848719D764440020F949 /* PreferencesGeneralViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreferencesGeneralViewController.h; sourceTree = ""; }; @@ -437,6 +452,7 @@ buildActionMask = 2147483647; files = ( CB32D2AE21902D9D00B8CD68 /* IOKit.framework in Frameworks */, + CB81AB2525B7EFA4006956F7 /* Sparkle.framework in Frameworks */, 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, CB587E500F50FE8800C66A09 /* SystemConfiguration.framework in Frameworks */, CBB3FD7A0F53834B00244132 /* Security.framework in Frameworks */, @@ -505,10 +521,10 @@ 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { isa = PBXGroup; children = ( - CBD4848219D75F6D0020F949 /* Sparkle.framework */, 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, CB587E4F0F50FE8800C66A09 /* SystemConfiguration.framework */, CB9E901D0F397FFA006DE6E4 /* Security.framework */, + CB81AB2325B7EF74006956F7 /* Sparkle.framework */, ); name = "Linked Frameworks"; sourceTree = ""; @@ -536,7 +552,7 @@ name = Products; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* SelfControl */ = { isa = PBXGroup; children = ( CBDFFF4B24A07DB300622CEE /* SelfControl.entitlements */, @@ -630,6 +646,7 @@ CB69C4ED25A3FD8A0030CFCD /* SCXPCAuthorization.m */, CB62FC3924B124B900ADBC40 /* SCXPCClient.h */, CB62FC3A24B124B900ADBC40 /* SCXPCClient.m */, + CB81AAB625B7E6C7006956F7 /* DeprecationSilencers.h */, ); path = Common; sourceTree = ""; @@ -803,6 +820,7 @@ files = ( CB81A94825B7B5B5006956F7 /* SCMigrationUtilities.h in Headers */, CB81AA3A25B7D152006956F7 /* SCHelperToolUtilities.h in Headers */, + CB81AAB725B7E6C7006956F7 /* DeprecationSilencers.h in Headers */, CB81A9CF25B7C269006956F7 /* SCBlockUtilities.h in Headers */, CB81A9F225B7C5F7006956F7 /* SCBlockFileReaderWriter.h in Headers */, ); @@ -824,6 +842,7 @@ CBDFFF4624A044C900622CEE /* Copy Daemon Launch Service */, CB5E5FF81C3A5FD10038F331 /* ShellScript */, 7D55904F6A3D627B837007A9 /* [CP] Embed Pods Frameworks */, + CB81AB2725B7EFA4006956F7 /* Embed Frameworks */, ); buildRules = ( ); @@ -992,7 +1011,7 @@ Base, da, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* SelfControl */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1598,7 +1617,6 @@ "$(inherited)", "$(SRCROOT)", "$(PROJECT_DIR)", - "$(PROJECT_DIR)/Pods/Sparkle", ); GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; @@ -1632,7 +1650,6 @@ "$(inherited)", "$(SRCROOT)", "$(PROJECT_DIR)", - "$(PROJECT_DIR)/Pods/Sparkle", ); GCC_MODEL_TUNING = G5; GCC_PRECOMPILE_PREFIX_HEADER = YES; diff --git a/SelfControl_Prefix.pch b/SelfControl_Prefix.pch index d57096bf..0af9d99b 100755 --- a/SelfControl_Prefix.pch +++ b/SelfControl_Prefix.pch @@ -8,6 +8,7 @@ #import #import "version-header.h" +#import "DeprecationSilencers.h" #import "SCUtility.h" #import "SCErr.h" #import "SCConstants.h" diff --git a/Sparkle.framework/Headers b/Sparkle.framework/Headers new file mode 120000 index 00000000..a177d2a6 --- /dev/null +++ b/Sparkle.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/Sparkle.framework/Modules b/Sparkle.framework/Modules new file mode 120000 index 00000000..5736f318 --- /dev/null +++ b/Sparkle.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/Sparkle.framework/PrivateHeaders b/Sparkle.framework/PrivateHeaders new file mode 120000 index 00000000..d8e56452 --- /dev/null +++ b/Sparkle.framework/PrivateHeaders @@ -0,0 +1 @@ +Versions/Current/PrivateHeaders \ No newline at end of file diff --git a/Sparkle.framework/Resources b/Sparkle.framework/Resources new file mode 120000 index 00000000..953ee36f --- /dev/null +++ b/Sparkle.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Sparkle.framework/Sparkle b/Sparkle.framework/Sparkle new file mode 120000 index 00000000..b2c52731 --- /dev/null +++ b/Sparkle.framework/Sparkle @@ -0,0 +1 @@ +Versions/Current/Sparkle \ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Headers/SPUDownloadData.h b/Sparkle.framework/Versions/A/Headers/SPUDownloadData.h new file mode 100644 index 00000000..41cd5743 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUDownloadData.h @@ -0,0 +1,43 @@ +// +// SPUDownloadData.h +// Sparkle +// +// Created by Mayur Pawashe on 8/10/16. +// Copyright © 2016 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +#import "SUExport.h" + +NS_ASSUME_NONNULL_BEGIN + +/*! + * A class for containing downloaded data along with some information about it. + */ +SU_EXPORT @interface SPUDownloadData : NSObject + +- (instancetype)initWithData:(NSData *)data textEncodingName:(NSString * _Nullable)textEncodingName MIMEType:(NSString * _Nullable)MIMEType; + +/*! + * The raw data that was downloaded. + */ +@property (nonatomic, readonly) NSData *data; + +/*! + * The IANA charset encoding name if available. Eg: "utf-8" + */ +@property (nonatomic, readonly, nullable, copy) NSString *textEncodingName; + +/*! + * The MIME type if available. Eg: "text/plain" + */ +@property (nonatomic, readonly, nullable, copy) NSString *MIMEType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Headers/SPUDownloader.h b/Sparkle.framework/Versions/A/Headers/SPUDownloader.h new file mode 100644 index 00000000..5eee9bd5 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUDownloader.h @@ -0,0 +1,25 @@ +// +// SPUDownloader.h +// Downloader +// +// Created by Mayur Pawashe on 4/1/16. +// Copyright © 2016 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SPUDownloaderProtocol.h" + +@protocol SPUDownloaderDelegate; + +// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. +@interface SPUDownloader : NSObject + +// Due to XPC remote object reasons, this delegate is strongly referenced +// Invoke cleanup when done with this instance +- (instancetype)initWithDelegate:(id )delegate; + +@end diff --git a/Sparkle.framework/Versions/A/Headers/SPUDownloaderDelegate.h b/Sparkle.framework/Versions/A/Headers/SPUDownloaderDelegate.h new file mode 100644 index 00000000..76e7e750 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUDownloaderDelegate.h @@ -0,0 +1,38 @@ +// +// SPUDownloaderDelegate.h +// Sparkle +// +// Created by Mayur Pawashe on 4/1/16. +// Copyright © 2016 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@class SPUDownloadData; + +@protocol SPUDownloaderDelegate + +// This is only invoked for persistent downloads +- (void)downloaderDidSetDestinationName:(NSString *)destinationName temporaryDirectory:(NSString *)temporaryDirectory; + +// Under rare cases, this may be called more than once, in which case the current progress should be reset back to 0 +// This is only invoked for persistent downloads +- (void)downloaderDidReceiveExpectedContentLength:(int64_t)expectedContentLength; + +// This is only invoked for persistent downloads +- (void)downloaderDidReceiveDataOfLength:(uint64_t)length; + +// downloadData is nil if this is a persisent download, otherwise it's non-nil if it's a temporary download +- (void)downloaderDidFinishWithTemporaryDownloadData:(SPUDownloadData * _Nullable)downloadData; + +- (void)downloaderDidFailWithError:(NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Headers/SPUDownloaderDeprecated.h b/Sparkle.framework/Versions/A/Headers/SPUDownloaderDeprecated.h new file mode 100644 index 00000000..36302df4 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUDownloaderDeprecated.h @@ -0,0 +1,13 @@ +// +// SPUDownloaderDeprecated.h +// Sparkle +// +// Created by Deadpikle on 12/20/17. +// Copyright © 2017 Sparkle Project. All rights reserved. +// + +#import "SPUDownloader.h" + +@interface SPUDownloaderDeprecated : SPUDownloader + +@end diff --git a/Sparkle.framework/Versions/A/Headers/SPUDownloaderProtocol.h b/Sparkle.framework/Versions/A/Headers/SPUDownloaderProtocol.h new file mode 100644 index 00000000..ebe477fe --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUDownloaderProtocol.h @@ -0,0 +1,34 @@ +// +// SPUDownloaderProtocol.h +// PersistentDownloader +// +// Created by Mayur Pawashe on 4/1/16. +// Copyright © 2016 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@class SPUURLRequest; + +// The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service. +@protocol SPUDownloaderProtocol + +- (void)startPersistentDownloadWithRequest:(SPUURLRequest *)request bundleIdentifier:(NSString *)bundleIdentifier desiredFilename:(NSString *)desiredFilename; + +- (void)startTemporaryDownloadWithRequest:(SPUURLRequest *)request; + +- (void)downloadDidFinish; + +- (void)cleanup; + +- (void)cancel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Headers/SPUDownloaderSession.h b/Sparkle.framework/Versions/A/Headers/SPUDownloaderSession.h new file mode 100644 index 00000000..4bde75aa --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUDownloaderSession.h @@ -0,0 +1,20 @@ +// +// SPUDownloaderSession.h +// Sparkle +// +// Created by Deadpikle on 12/20/17. +// Copyright © 2017 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SPUDownloader.h" +#import "SPUDownloaderProtocol.h" + +NS_CLASS_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0) +@interface SPUDownloaderSession : SPUDownloader + +@end diff --git a/Sparkle.framework/Versions/A/Headers/SPUURLRequest.h b/Sparkle.framework/Versions/A/Headers/SPUURLRequest.h new file mode 100644 index 00000000..69496147 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SPUURLRequest.h @@ -0,0 +1,35 @@ +// +// SPUURLRequest.h +// Sparkle +// +// Created by Mayur Pawashe on 5/19/16. +// Copyright © 2016 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +// A class that wraps NSURLRequest and implements NSSecureCoding +// This class exists because NSURLRequest did not support NSSecureCoding in macOS 10.8 +// I have not verified if NSURLRequest in 10.9 implements NSSecureCoding or not +@interface SPUURLRequest : NSObject + +// Creates a new URL request +// Only these properties are currently tracked: +// * URL +// * Cache policy +// * Timeout interval +// * HTTP header fields +// * networkServiceType ++ (instancetype)URLRequestWithRequest:(NSURLRequest *)request; + +@property (nonatomic, readonly) NSURLRequest *request; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Headers/SUAppcast.h b/Sparkle.framework/Versions/A/Headers/SUAppcast.h new file mode 100644 index 00000000..34276b7d --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUAppcast.h @@ -0,0 +1,35 @@ +// +// SUAppcast.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SUAPPCAST_H +#define SUAPPCAST_H + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SUAppcastItem; +SU_EXPORT @interface SUAppcast : NSObject + +@property (copy, nullable) NSString *userAgentString; +@property (copy, nullable) NSDictionary *httpHeaders; + +- (void)fetchAppcastFromURL:(NSURL *)url inBackground:(BOOL)bg completionBlock:(void (^)(NSError *_Nullable))err; +- (SUAppcast *)copyWithoutDeltaUpdates; + +@property (readonly, copy, nullable) NSArray *items; +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h b/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h new file mode 100644 index 00000000..1d8b1d01 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h @@ -0,0 +1,54 @@ +// +// SUAppcastItem.h +// Sparkle +// +// Created by Andy Matuschak on 3/12/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SUAPPCASTITEM_H +#define SUAPPCASTITEM_H + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" +@class SUSignatures; + +SU_EXPORT @interface SUAppcastItem : NSObject +@property (copy, readonly) NSString *title; +@property (copy, readonly) NSString *dateString; +@property (copy, readonly) NSDate *date; +@property (copy, readonly) NSString *itemDescription; +@property (strong, readonly) NSURL *releaseNotesURL; +@property (strong, readonly) SUSignatures *signatures; +@property (copy, readonly) NSString *minimumSystemVersion; +@property (copy, readonly) NSString *maximumSystemVersion; +@property (strong, readonly) NSURL *fileURL; +@property (nonatomic, readonly) uint64_t contentLength; +@property (copy, readonly) NSString *versionString; +@property (copy, readonly) NSString *osString; +@property (copy, readonly) NSString *displayVersionString; +@property (copy, readonly) NSDictionary *deltaUpdates; +@property (strong, readonly) NSURL *infoURL; +@property (copy, readonly) NSNumber* phasedRolloutInterval; + +// Initializes with data from a dictionary provided by the RSS class. +- (instancetype)initWithDictionary:(NSDictionary *)dict; +- (instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString **)error; + +@property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; +@property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; +@property (getter=isMacOsUpdate, readonly) BOOL macOsUpdate; +@property (getter=isInformationOnlyUpdate, readonly) BOOL informationOnlyUpdate; + +// Returns the dictionary provided in initWithDictionary; this might be useful later for extensions. +@property (readonly, copy) NSDictionary *propertiesDictionary; + +- (NSURL *)infoURL; + +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUCodeSigningVerifier.h b/Sparkle.framework/Versions/A/Headers/SUCodeSigningVerifier.h new file mode 100644 index 00000000..3756a378 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUCodeSigningVerifier.h @@ -0,0 +1,26 @@ +// +// SUCodeSigningVerifier.h +// Sparkle +// +// Created by Andy Matuschak on 7/5/12. +// +// + +#ifndef SUCODESIGNINGVERIFIER_H +#define SUCODESIGNINGVERIFIER_H + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" + +SU_EXPORT @interface SUCodeSigningVerifier : NSObject ++ (BOOL)codeSignatureAtBundleURL:(NSURL *)oldBundlePath matchesSignatureAtBundleURL:(NSURL *)newBundlePath error:(NSError **)error; ++ (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundlePath error:(NSError **)error; ++ (BOOL)bundleAtURLIsCodeSigned:(NSURL *)bundlePath; ++ (NSDictionary *)codeSignatureInfoAtBundleURL:(NSURL *)bundlePath; +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUErrors.h b/Sparkle.framework/Versions/A/Headers/SUErrors.h new file mode 100644 index 00000000..4b160c4f --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUErrors.h @@ -0,0 +1,57 @@ +// +// SUErrors.h +// Sparkle +// +// Created by C.W. Betts on 10/13/14. +// Copyright (c) 2014 Sparkle Project. All rights reserved. +// + +#ifndef SUERRORS_H +#define SUERRORS_H + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" + +/** + * Error domain used by Sparkle + */ +SU_EXPORT extern NSString *const SUSparkleErrorDomain; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +typedef NS_ENUM(OSStatus, SUError) { + // Appcast phase errors. + SUAppcastParseError = 1000, + SUNoUpdateError = 1001, + SUAppcastError = 1002, + SURunningFromDiskImageError = 1003, + SURunningTranslocated = 1004, + + // Download phase errors. + SUTemporaryDirectoryError = 2000, + SUDownloadError = 2001, + + // Extraction phase errors. + SUUnarchivingError = 3000, + SUSignatureError = 3001, + + // Installation phase errors. + SUFileCopyFailure = 4000, + SUAuthenticationFailure = 4001, + SUMissingUpdateError = 4002, + SUMissingInstallerToolError = 4003, + SURelaunchError = 4004, + SUInstallationError = 4005, + SUDowngradeError = 4006, + SUInstallationCancelledError = 4007, + + // System phase errors + SUSystemPowerOffError = 5000 +}; +#pragma clang diagnostic pop + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUExport.h b/Sparkle.framework/Versions/A/Headers/SUExport.h new file mode 100644 index 00000000..3e3f8a16 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUExport.h @@ -0,0 +1,18 @@ +// +// SUExport.h +// Sparkle +// +// Created by Jake Petroules on 2014-08-23. +// Copyright (c) 2014 Sparkle Project. All rights reserved. +// + +#ifndef SUEXPORT_H +#define SUEXPORT_H + +#ifdef BUILDING_SPARKLE +#define SU_EXPORT __attribute__((visibility("default"))) +#else +#define SU_EXPORT +#endif + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h b/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h new file mode 100644 index 00000000..ed11921a --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h @@ -0,0 +1,52 @@ +// +// SUStandardVersionComparator.h +// Sparkle +// +// Created by Andy Matuschak on 12/21/07. +// Copyright 2007 Andy Matuschak. All rights reserved. +// + +#ifndef SUSTANDARDVERSIONCOMPARATOR_H +#define SUSTANDARDVERSIONCOMPARATOR_H + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" +#import "SUVersionComparisonProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +/*! + Sparkle's default version comparator. + + This comparator is adapted from MacPAD, by Kevin Ballard. + It's "dumb" in that it does essentially string comparison, + in components split by character type. +*/ +SU_EXPORT @interface SUStandardVersionComparator : NSObject + +/*! + Initializes a new instance of the standard version comparator. + */ +- (instancetype)init; + +/*! + Returns a singleton instance of the comparator. + + It is usually preferred to alloc/init new a comparator instead. +*/ ++ (SUStandardVersionComparator *)defaultComparator; + +/*! + Compares version strings through textual analysis. + + See the implementation for more details. +*/ +- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; +@end + +NS_ASSUME_NONNULL_END +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUUpdater.h b/Sparkle.framework/Versions/A/Headers/SUUpdater.h new file mode 100644 index 00000000..d05270f2 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUUpdater.h @@ -0,0 +1,233 @@ +// +// SUUpdater.h +// Sparkle +// +// Created by Andy Matuschak on 1/4/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SUUPDATER_H +#define SUUPDATER_H + +#if __has_feature(modules) +@import Cocoa; +#else +#import +#endif +#import "SUExport.h" +#import "SUVersionComparisonProtocol.h" +#import "SUVersionDisplayProtocol.h" + +@class SUAppcastItem, SUAppcast; + +@protocol SUUpdaterDelegate; + +/*! + The main API in Sparkle for controlling the update mechanism. + + This class is used to configure the update paramters as well as manually + and automatically schedule and control checks for updates. + */ +SU_EXPORT @interface SUUpdater : NSObject + +@property (unsafe_unretained) IBOutlet id delegate; + +/*! + The shared updater for the main bundle. + + This is equivalent to passing [NSBundle mainBundle] to SUUpdater::updaterForBundle: + */ ++ (SUUpdater *)sharedUpdater; + +/*! + The shared updater for a specified bundle. + + If an updater has already been initialized for the provided bundle, that shared instance will be returned. + */ ++ (SUUpdater *)updaterForBundle:(NSBundle *)bundle; + +/*! + Designated initializer for SUUpdater. + + If an updater has already been initialized for the provided bundle, that shared instance will be returned. + */ +- (instancetype)initForBundle:(NSBundle *)bundle; + +/*! + Explicitly checks for updates and displays a progress dialog while doing so. + + This method is meant for a main menu item. + Connect any menu item to this action in Interface Builder, + and Sparkle will check for updates and report back its findings verbosely + when it is invoked. + + This will find updates that the user has opted into skipping. + */ +- (IBAction)checkForUpdates:(id)sender; + +/*! + The menu item validation used for the -checkForUpdates: action + */ +- (BOOL)validateMenuItem:(NSMenuItem *)menuItem; + +/*! + Checks for updates, but does not display any UI unless an update is found. + + This is meant for programmatically initating a check for updates. That is, + it will display no UI unless it actually finds an update, in which case it + proceeds as usual. + + If automatic downloading of updates it turned on and allowed, however, + this will invoke that behavior, and if an update is found, it will be downloaded + in the background silently and will be prepped for installation. + + This will not find updates that the user has opted into skipping. + */ +- (void)checkForUpdatesInBackground; + +/*! + A property indicating whether or not to check for updates automatically. + + Setting this property will persist in the host bundle's user defaults. + The update schedule cycle will be reset in a short delay after the property's new value is set. + This is to allow reverting this property without kicking off a schedule change immediately + */ +@property BOOL automaticallyChecksForUpdates; + +/*! + A property indicating whether or not updates can be automatically downloaded in the background. + + Note that automatic downloading of updates can be disallowed by the developer + or by the user's system if silent updates cannot be done (eg: if they require authentication). + In this case, -automaticallyDownloadsUpdates will return NO regardless of how this property is set. + + Setting this property will persist in the host bundle's user defaults. + */ +@property BOOL automaticallyDownloadsUpdates; + +/*! + A property indicating the current automatic update check interval. + + Setting this property will persist in the host bundle's user defaults. + The update schedule cycle will be reset in a short delay after the property's new value is set. + This is to allow reverting this property without kicking off a schedule change immediately + */ +@property NSTimeInterval updateCheckInterval; + +/*! + Begins a "probing" check for updates which will not actually offer to + update to that version. + + However, the delegate methods + SUUpdaterDelegate::updater:didFindValidUpdate: and + SUUpdaterDelegate::updaterDidNotFindUpdate: will be called, + so you can use that information in your UI. + + Updates that have been skipped by the user will not be found. + */ +- (void)checkForUpdateInformation; + +/*! + The URL of the appcast used to download update information. + + Setting this property will persist in the host bundle's user defaults. + If you don't want persistence, you may want to consider instead implementing + SUUpdaterDelegate::feedURLStringForUpdater: or SUUpdaterDelegate::feedParametersForUpdater:sendingSystemProfile: + + This property must be called on the main thread. + */ +@property (copy) NSURL *feedURL; + +/*! + The host bundle that is being updated. + */ +@property (readonly, strong) NSBundle *hostBundle; + +/*! + The bundle this class (SUUpdater) is loaded into. + */ +@property (strong, readonly) NSBundle *sparkleBundle; + +/*! + The user agent used when checking for updates. + + The default implementation can be overrided. + */ +@property (nonatomic, copy) NSString *userAgentString; + +/*! + The HTTP headers used when checking for updates. + + The keys of this dictionary are HTTP header fields (NSString) and values are corresponding values (NSString) + */ +@property (copy) NSDictionary *httpHeaders; + +/*! + A property indicating whether or not the user's system profile information is sent when checking for updates. + + Setting this property will persist in the host bundle's user defaults. + */ +@property BOOL sendsSystemProfile; + +/*! + A property indicating the decryption password used for extracting updates shipped as Apple Disk Images (dmg) + */ +@property (nonatomic, copy) NSString *decryptionPassword; + +/*! + This function ignores normal update schedule, ignores user preferences, + and interrupts users with an unwanted immediate app update. + + WARNING: this function should not be used in regular apps. This function + is a user-unfriendly hack only for very special cases, like unstable + rapidly-changing beta builds that would not run correctly if they were + even one day out of date. + + Instead of this function you should set `SUAutomaticallyUpdate` to `YES`, + which will gracefully install updates when the app quits. + + For UI-less/daemon apps that aren't usually quit, instead of this function, + you can use the delegate method + SUUpdaterDelegate::updater:willInstallUpdateOnQuit:immediateInstallationInvocation: + or + SUUpdaterDelegate::updater:willInstallUpdateOnQuit:immediateInstallationBlock: + to immediately start installation when an update was found. + + A progress dialog is shown but the user will never be prompted to read the + release notes. + + This function will cause update to be downloaded twice if automatic updates are + enabled. + + You may want to respond to the userDidCancelDownload delegate method in case + the user clicks the "Cancel" button while the update is downloading. + */ +- (void)installUpdatesIfAvailable; + +/*! + Returns the date of last update check. + + \returns \c nil if no check has been performed. + */ +@property (readonly, copy) NSDate *lastUpdateCheckDate; + +/*! + Appropriately schedules or cancels the update checking timer according to + the preferences for time interval and automatic checks. + + This call does not change the date of the next check, + but only the internal NSTimer. + */ +- (void)resetUpdateCycle; + +/*! + A property indicating whether or not an update is in progress. + + Note this property is not indicative of whether or not user initiated updates can be performed. + Use SUUpdater::validateMenuItem: for that instead. + */ +@property (readonly) BOOL updateInProgress; + +@end + +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h b/Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h new file mode 100644 index 00000000..ec844d04 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h @@ -0,0 +1,352 @@ +// +// SUUpdaterDelegate.h +// Sparkle +// +// Created by Mayur Pawashe on 12/25/16. +// Copyright © 2016 Sparkle Project. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +#import "SUExport.h" + +@protocol SUVersionComparison, SUVersionDisplay; +@class SUUpdater, SUAppcast, SUAppcastItem; + +NS_ASSUME_NONNULL_BEGIN + +// ----------------------------------------------------------------------------- +// SUUpdater Notifications for events that might be interesting to more than just the delegate +// The updater will be the notification object +// ----------------------------------------------------------------------------- +SU_EXPORT extern NSString *const SUUpdaterDidFinishLoadingAppCastNotification; +SU_EXPORT extern NSString *const SUUpdaterDidFindValidUpdateNotification; +SU_EXPORT extern NSString *const SUUpdaterDidNotFindUpdateNotification; +SU_EXPORT extern NSString *const SUUpdaterWillRestartNotification; +#define SUUpdaterWillRelaunchApplicationNotification SUUpdaterWillRestartNotification; +#define SUUpdaterWillInstallUpdateNotification SUUpdaterWillRestartNotification; + +// Key for the SUAppcastItem object in the SUUpdaterDidFindValidUpdateNotification userInfo +SU_EXPORT extern NSString *const SUUpdaterAppcastItemNotificationKey; +// Key for the SUAppcast object in the SUUpdaterDidFinishLoadingAppCastNotification userInfo +SU_EXPORT extern NSString *const SUUpdaterAppcastNotificationKey; + +// ----------------------------------------------------------------------------- +// SUUpdater Delegate: +// ----------------------------------------------------------------------------- + +/*! + Provides methods to control the behavior of an SUUpdater object. + */ +@protocol SUUpdaterDelegate +@optional + +/*! + Returns whether to allow Sparkle to pop up. + + For example, this may be used to prevent Sparkle from interrupting a setup assistant. + + \param updater The SUUpdater instance. + */ +- (BOOL)updaterMayCheckForUpdates:(SUUpdater *)updater; + +/*! + Returns additional parameters to append to the appcast URL's query string. + + This is potentially based on whether or not Sparkle will also be sending along the system profile. + + \param updater The SUUpdater instance. + \param sendingProfile Whether the system profile will also be sent. + + \return An array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. + */ +- (NSArray *> *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; + +/*! + Returns a custom appcast URL. + + Override this to dynamically specify the entire URL. + + An alternative may be to use SUUpdaterDelegate::feedParametersForUpdater:sendingSystemProfile: + and let the server handle what kind of feed to provide. + + \param updater The SUUpdater instance. + */ +- (nullable NSString *)feedURLStringForUpdater:(SUUpdater *)updater; + +/*! + Returns whether Sparkle should prompt the user about automatic update checks. + + Use this to override the default behavior. + + \param updater The SUUpdater instance. + */ +- (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SUUpdater *)updater; + +/*! + Called after Sparkle has downloaded the appcast from the remote server. + + Implement this if you want to do some special handling with the appcast once it finishes loading. + + \param updater The SUUpdater instance. + \param appcast The appcast that was downloaded from the remote server. + */ +- (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; + +/*! + Returns the item in the appcast corresponding to the update that should be installed. + + If you're using special logic or extensions in your appcast, + implement this to use your own logic for finding a valid update, if any, + in the given appcast. + + \param appcast The appcast that was downloaded from the remote server. + \param updater The SUUpdater instance. + */ +- (nullable SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SUUpdater *)updater; + +/*! + Called when a valid update is found by the update driver. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + */ +- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item; + +/*! + Called just before the scheduled update driver prompts the user to install an update. + + \param updater The SUUpdater instance. + + \return YES to allow the update prompt to be shown (the default behavior), or NO to suppress it. + */ +- (BOOL)updaterShouldShowUpdateAlertForScheduledUpdate:(SUUpdater *)updater forItem:(SUAppcastItem *)item; + +/*! + Called after the user dismisses the update alert. + + \param updater The SUUpdater instance. + \param permanently YES if the alert will not appear again for this update; NO if it may reappear. + */ +- (void)updater:(SUUpdater *)updater didDismissUpdateAlertPermanently:(BOOL)permanently forItem:(SUAppcastItem *)item; + +/*! + Called when a valid update is not found. + + \param updater The SUUpdater instance. + */ +- (void)updaterDidNotFindUpdate:(SUUpdater *)updater; + +/*! + Called when the user clicks the Skip This Version button. + + \param updater The SUUpdater instance. + */ +- (void)updater:(SUUpdater *)updater userDidSkipThisVersion:(SUAppcastItem *)item; + +/*! + Called immediately before downloading the specified update. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be downloaded. + \param request The mutable URL request that will be used to download the update. + */ +- (void)updater:(SUUpdater *)updater willDownloadUpdate:(SUAppcastItem *)item withRequest:(NSMutableURLRequest *)request; + +/*! + Called immediately after succesfull download of the specified update. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that has been downloaded. + */ +- (void)updater:(SUUpdater *)updater didDownloadUpdate:(SUAppcastItem *)item; + +/*! + Called after the specified update failed to download. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that failed to download. + \param error The error generated by the failed download. + */ +- (void)updater:(SUUpdater *)updater failedToDownloadUpdate:(SUAppcastItem *)item error:(NSError *)error; + +/*! + Called when the user clicks the cancel button while and update is being downloaded. + + \param updater The SUUpdater instance. + */ +- (void)userDidCancelDownload:(SUUpdater *)updater; + +/*! + Called immediately before extracting the specified downloaded update. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be extracted. + */ +- (void)updater:(SUUpdater *)updater willExtractUpdate:(SUAppcastItem *)item; + +/*! + Called immediately after extracting the specified downloaded update. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that has been extracted. + */ +- (void)updater:(SUUpdater *)updater didExtractUpdate:(SUAppcastItem *)item; + +/*! + Called immediately before installing the specified update. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + */ +- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)item; + +/*! + Returns whether the relaunch should be delayed in order to perform other tasks. + + This is not called if the user didn't relaunch on the previous update, + in that case it will immediately restart. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + \param invocation The invocation that must be completed with `[invocation invoke]` before continuing with the relaunch. + + \return \c YES to delay the relaunch until \p invocation is invoked. + */ +- (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item untilInvoking:(NSInvocation *)invocation; + +/*! + Returns whether the relaunch should be delayed in order to perform other tasks. + + This is not called if the user didn't relaunch on the previous update, + in that case it will immediately restart. + + This method acts as a simpler alternative to SUUpdaterDelegate::updater:shouldPostponeRelaunchForUpdate:untilInvoking: avoiding usage of NSInvocation, which is not available in Swift environments. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + + \return \c YES to delay the relaunch. + */ +- (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item; + +/*! + Returns whether the application should be relaunched at all. + + Some apps \b cannot be relaunched under certain circumstances. + This method can be used to explicitly prevent a relaunch. + + \param updater The SUUpdater instance. + */ +- (BOOL)updaterShouldRelaunchApplication:(SUUpdater *)updater; + +/*! + Called immediately before relaunching. + + \param updater The SUUpdater instance. + */ +- (void)updaterWillRelaunchApplication:(SUUpdater *)updater; + +/*! + Called immediately after relaunching. SUUpdater delegate must be set before applicationDidFinishLaunching: to catch this event. + + \param updater The SUUpdater instance. + */ +- (void)updaterDidRelaunchApplication:(SUUpdater *)updater; + +/*! + Returns an object that compares version numbers to determine their arithmetic relation to each other. + + This method allows you to provide a custom version comparator. + If you don't implement this method or return \c nil, + the standard version comparator will be used. + + \sa SUStandardVersionComparator + + \param updater The SUUpdater instance. + */ +- (nullable id)versionComparatorForUpdater:(SUUpdater *)updater; + +/*! + Returns an object that formats version numbers for display to the user. + + If you don't implement this method or return \c nil, + the standard version formatter will be used. + + \sa SUUpdateAlert + + \param updater The SUUpdater instance. + */ +- (nullable id)versionDisplayerForUpdater:(SUUpdater *)updater; + +/*! + Returns the path which is used to relaunch the client after the update is installed. + + The default is the path of the host bundle. + + \param updater The SUUpdater instance. + */ +- (nullable NSString *)pathToRelaunchForUpdater:(SUUpdater *)updater; + +/*! + Called before an updater shows a modal alert window, + to give the host the opportunity to hide attached windows that may get in the way. + + \param updater The SUUpdater instance. + */ +- (void)updaterWillShowModalAlert:(SUUpdater *)updater; + +/*! + Called after an updater shows a modal alert window, + to give the host the opportunity to hide attached windows that may get in the way. + + \param updater The SUUpdater instance. + */ +- (void)updaterDidShowModalAlert:(SUUpdater *)updater; + +/*! + Called when an update is scheduled to be silently installed on quit. + This is after an update has been automatically downloaded in the background. + (i.e. SUUpdater::automaticallyDownloadsUpdates is YES) + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + \param invocation Can be used to trigger an immediate silent install and relaunch. + */ +- (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationInvocation:(NSInvocation *)invocation; + +/*! + Called when an update is scheduled to be silently installed on quit. + This is after an update has been automatically downloaded in the background. + (i.e. SUUpdater::automaticallyDownloadsUpdates is YES) + This method acts as a more modern alternative to SUUpdaterDelegate::updater:willInstallUpdateOnQuit:immediateInstallationInvocation: using a block instead of NSInvocation, which is not available in Swift environments. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that is proposed to be installed. + \param installationBlock Can be used to trigger an immediate silent install and relaunch. + */ +- (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationBlock:(void (^)(void))installationBlock; + +/*! + Calls after an update that was scheduled to be silently installed on quit has been canceled. + + \param updater The SUUpdater instance. + \param item The appcast item corresponding to the update that was proposed to be installed. + */ +- (void)updater:(SUUpdater *)updater didCancelInstallUpdateOnQuit:(SUAppcastItem *)item; + +/*! + Called after an update is aborted due to an error. + + \param updater The SUUpdater instance. + \param error The error that caused the abort + */ +- (void)updater:(SUUpdater *)updater didAbortWithError:(NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h b/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h new file mode 100644 index 00000000..c654fc4d --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h @@ -0,0 +1,37 @@ +// +// SUVersionComparisonProtocol.h +// Sparkle +// +// Created by Andy Matuschak on 12/21/07. +// Copyright 2007 Andy Matuschak. All rights reserved. +// + +#ifndef SUVERSIONCOMPARISONPROTOCOL_H +#define SUVERSIONCOMPARISONPROTOCOL_H + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" + +NS_ASSUME_NONNULL_BEGIN + +/*! + Provides version comparison facilities for Sparkle. +*/ +@protocol SUVersionComparison + +/*! + An abstract method to compare two version strings. + + Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, + and NSOrderedSame if they are equivalent. +*/ +- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; // *** MAY BE CALLED ON NON-MAIN THREAD! + +@end + +NS_ASSUME_NONNULL_END +#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h b/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h new file mode 100644 index 00000000..980efb3f --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h @@ -0,0 +1,29 @@ +// +// SUVersionDisplayProtocol.h +// EyeTV +// +// Created by Uli Kusterer on 08.12.09. +// Copyright 2009 Elgato Systems GmbH. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "SUExport.h" + +/*! + Applies special display formatting to version numbers. +*/ +@protocol SUVersionDisplay + +/*! + Formats two version strings. + + Both versions are provided so that important distinguishing information + can be displayed while also leaving out unnecessary/confusing parts. +*/ +- (void)formatVersion:(NSString *_Nonnull*_Nonnull)inOutVersionA andVersion:(NSString *_Nonnull*_Nonnull)inOutVersionB; + +@end diff --git a/Sparkle.framework/Versions/A/Headers/Sparkle.h b/Sparkle.framework/Versions/A/Headers/Sparkle.h new file mode 100644 index 00000000..1085d419 --- /dev/null +++ b/Sparkle.framework/Versions/A/Headers/Sparkle.h @@ -0,0 +1,39 @@ +// +// Sparkle.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#ifndef SPARKLE_H +#define SPARKLE_H + +// This list should include the shared headers. It doesn't matter if some of them aren't shared (unless +// there are name-space collisions) so we can list all of them to start with: + +#pragma clang diagnostic push +// Do not use <> style includes since 2.x has two frameworks that need to work: Sparkle and SparkleCore +#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" + +#import "SUAppcast.h" +#import "SUAppcastItem.h" +#import "SUStandardVersionComparator.h" +#import "SUUpdater.h" +#import "SUUpdaterDelegate.h" +#import "SUVersionComparisonProtocol.h" +#import "SUVersionDisplayProtocol.h" +#import "SUErrors.h" + +#import "SPUDownloader.h" +#import "SPUDownloaderDelegate.h" +#import "SPUDownloaderDeprecated.h" +#import "SPUDownloadData.h" +#import "SPUDownloaderProtocol.h" +#import "SPUDownloaderSession.h" +#import "SPUURLRequest.h" +#import "SUCodeSigningVerifier.h" + +#pragma clang diagnostic pop + +#endif diff --git a/Sparkle.framework/Versions/A/Modules/module.modulemap b/Sparkle.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 00000000..af3fe6d0 --- /dev/null +++ b/Sparkle.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module Sparkle { + umbrella header "Sparkle.h" + + export * + module * { export * } +} diff --git a/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h b/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h new file mode 100644 index 00000000..a52bf5a2 --- /dev/null +++ b/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h @@ -0,0 +1,21 @@ +// +// SUUnarchiver.h +// Sparkle +// +// Created by Andy Matuschak on 3/16/06. +// Copyright 2006 Andy Matuschak. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol SUUnarchiverProtocol; + +@interface SUUnarchiver : NSObject + ++ (nullable id )unarchiverForPath:(NSString *)path updatingHostBundlePath:(nullable NSString *)hostPath decryptionPassword:(nullable NSString *)decryptionPassword; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist new file mode 100644 index 00000000..676181f9 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist @@ -0,0 +1,56 @@ + + + + + BuildMachineOSBuild + 20B28 + CFBundleDevelopmentRegion + English + CFBundleExecutable + Autoupdate + CFBundleIconFile + AppIcon.icns + CFBundleIdentifier + org.sparkle-project.Sparkle.Autoupdate + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.24.0 a-67-g0e162c98 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1.24.0 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12C5020f + DTPlatformName + macosx + DTPlatformVersion + 11.1 + DTSDKBuild + 20C5048g + DTSDKName + macosx11.1 + DTXcode + 1230 + DTXcodeBuild + 12C5020f + LSBackgroundOnly + 1 + LSMinimumSystemVersion + 10.7 + LSUIElement + 1 + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate new file mode 100755 index 0000000000000000000000000000000000000000..164511d4f4f93fc2de35af03c566d3af70748a7e GIT binary patch literal 577184 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Ȃ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 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/fileop b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/fileop new file mode 100755 index 0000000000000000000000000000000000000000..29dae8a10125115a94ecad33c5b5497232769672 GIT binary patch literal 284960 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�TvNK3r*%@w;= z4cpN7fhSHoI@aEA$o`G%+p62|jr(H9wjUagy!OO3@AfMB$JE9jTx0J4<@fToMfbvi z;R{y1wSQ&tx|CNgKj*WLj-EAn(WKv;<6c}csZac&$9Dhv!Qk4-qbAO}W6Xi3gG^anCrVedyjoihjFUq+9aU+uBam2bq(?K@;}T3BKTZ{@jtr<9Bv zl+kz0c_)od&YPS|w=~&nJUGx{FPxH7Fd^68cTj4o*x*e~%`3^X7Zyw?F0n&KYUxOD2g zlTI8xTdzA0B3M`eSHIvUpt`|$1wAN;d%gzIL6mu;Q;7`JL4U2D3hq?NJ~~whKkp<| z-H)md9g>oEM(5`3fU|9y^R=(jJNmT_{=stYEK>GRv^ zNzc7CGOOXGD@Lu4{o&>0+%8jxp*&Y3ElhkOb7I#|s#8~-^;+>` zJwCtf!IV24pC6O?cO+-J|_?P3Q?>swo-GGmqVsefq#U1T)P4KN8f2UESuI#mR z_PuWoy5o(!w11wQ^!3=0!`*wTPkSNz;Gi3ij99UA;E1~h1RnWi`3Euiug<>R=Y4$4 zyj9ch?{ojxyJ9Z7Fyq3bu~$$1)2z$h&!{-_6m4ji6?a!2nBRMJlQX~fmCszKUD3Pq zJEx4D|LY9f>AGCQ@)F-{ao%Go2+TyJzAKrL(X1^!@ z_1?D~o1Z=X-WTrK^3D23N+*8zw*I=~r$;V4uu?(JoUYk4sX|9{l|9Ss(+$F9Hc8S?S_zZB*j{QL3r`=3pJ?wU$R{(btUg7oe;xkl_Q=z9A8 z4jpQ|qVyzk<*j?Ah}7aZ*KWT&LW zr|0~<{P{0ezok7Ccz1cnH*V}W@{13G_K7=UzkO}Uy*oyX@3br7x41)3Z!h2YNdC7g z&yGpE{{1JHd;1mqGWL>%k6s&-xN64&Juq$0cT>OlOONh<^*3JoO40oHeto;@xy#?3 zdc~V3ulOlDX-M+fr#zuM9!|LA!L#}tJP`HWyr#wjFRs0C>r&^_!}@n0ar9F$1BpFe zKeEy-4OkkmG+=4K(txD_O9Pe$EDcy1ury$4z|w%F0ZRjx1}qI&8n85AX~5Eer2$I= zmIf>hSQ@Z2U}?b8fTaOT1C|CX4OkkmG+=4K(txD_O9Pe$EDcy1ury$4z|w%F0ZRjx z1}qI&8n85AX~5Eer2$I=mIf>hSQ@Z2U}?b8fTaOT1C|CX4OkkmG+=4qe~kwI57s_K AhyVZp literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo new file mode 100644 index 00000000..bd04210f --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo @@ -0,0 +1 @@ +APPL???? \ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns new file mode 100644 index 0000000000000000000000000000000000000000..7f2a571c80597f750bfc38a1d586372fe28f665a GIT binary patch literal 37132 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 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib new file mode 100644 index 0000000000000000000000000000000000000000..f9c39a03214d3d57cf7158501fbbf896310e4a10 GIT binary patch literal 12667 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 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..02e077cf5d4b2ffb6b3d6be567187269610d2b9c GIT binary patch literal 10638 zcmeI2$!=W76^5&qG2m5JhBntk6gfdiA<5DRV#cx)53(IebRZ*}=IG|oB)Zw6#f6^5 zug0YpdVv5=@_+U7sOkR-U#0g_qo+SyK7F0W zX|9&dw4={dPuuA@o$2XN|HpdU(YMjlH2`_J+4sD7S)@&~KeKLYiypPkj zX+O_9Ne@R8tu|BJBel5yK>eI~k)EjcG(FWE-ue4*^fZk}v>oM@Pc(}68>6Y(%*SKR zVYK&m)*oMio-JP+PN`6Q38&r3@=?^LH`=O)u$bNN^& zLPkFCx%z2Y=zXqzko#$t0-Sv}y`j&m(zJhM-u_=-tS?`@HZtFmzD(1ttTW&W=uj+u zJN-^Av@_1Q5q5m07SQNey1JO@46B&-v zp1#mnOV2t#7gzo!?1Z91Dcl94kp7d=L0+Yg_{bt~Z0B-~eeGjzb6VsQKCaDs#snJH zJpH@q@#%sZP9#kd|Dn-+oW<+f*TMIf!_TX7b3^_F`*>6O-sUFscexa2gA1JM$8fr9 zOL#Ezeu%GSMHI3v~|28%eR*4&$y(IGLSqqi;XKFijNwi<22 z`Z>>;zMxf;b$g~~V=Ouc+DtTOEEy5G)O;#jDUp_`W7U0032V7A+E-LWR08+e2Tw8; z-o}3DUO}^$P5(c&_dDsKXd(W=zo7X;JnHHj(x|xurEBRol_$DRj;y8Yr1XzDS1`5q z8@JPMb?!Sl(_E*YNy;aZO-LS%`zGrIUI1LXpEmRMxD-WiuR&KXIiql>9>H=X2eP$TDowgdWZ9)C@WOl6dB(>&Un6Wq zi?J$lfO9<=>xSgMsC1+K8(F3ihs2- zp5};QCNEjyG)|3vfa2tG(B98a*VOjYXaJZ72B5omccTHC89at=<7iu_@v=qV!O+cC zW;&!3fsT=6GU^&%#JG1}0&b}gnxjl;ZhaOJZ>iYy1~7nOsdN(P)L zSmZdZO*0yek0bv9^U1l2XC#7Q1!n?8^%ISLl-r2M+V;`boA4}a8ux{G$9bK#^s@uv zI9HTrt#|``bYw~PbRelXYa_P!SJv5mWqMHkt>*XZD$MnskK{L4;j!$%bs;KEk7X-k z?bxKR)aR-QsBc?wRybGYMigfJ-_Fs^BvV-=|HE9wUlLWZde?T~8{8c7q}RbcTO%1! z;U0PAB4Zvg&+Et~Mi@1|SipJKhvK!-vwEl3lT+{};teqB zgwA|u*el->MYn(v8gs9p*cSnM8X?^icx_N z?u0+4lCY^>;)y`1Wqz2*&sBcs_lOOtwpx-B9TSJLe4=#PtYmX&LXemZiZ}=3Qyz! zf2krfp2N4kFFA25gnpK@d?oZpnyfoKwrORq!!wszXuTp??w0I0EF#WOM?Q1m1fR0Y z=R$=Wqlcr#v)WBxXIvQ>rr~M&brV#Wn-4OYunJW)=Pc-`Es_`;G$NAQ6{nm(hXm{! z&{a5&eNr=K41O~tApKBFxj~E**C*|s^S<^U%L}*9IfQ#VKf#GU$n}hF1o6D{+!GHX z2KX^&3Xb=6eotJd7Cxl=e@(Ua`z`eUBT1zAcC^Jey7#h06vQsNSI(8!|jnmmp5C1#n(V!QcPWNg+tvbUOpZ=_#jTcm`FP&PEqQD;rhvNfnEg3$hThdI{2vnmv$U5u_{2PE446$lZj zeFy)S#jQge@C_<{fOy3I!1WI_6WPIYw1#I^=;tg?GjASTa+kUox5U_2uAt_dc2E$# z-^eark%KHCZpTa z+TE;;xM_Li6Plw!i#1?JWOP%}?1~T3{)u`nGTn8|zM=N`9aNJ(&`j6R>KfzwI)`ga zB`+D`@Fds!!snNjv0*Ah?JDVdM@DUb=&?1T-Kwa&rPaEaHguOmUWJVmyTN+Ho7Hwo zyRkMTgw^010>Mf>(Z3+G-wN!HT-%-rDa5Z($gr8=8x?eun~}Cw7#ZGCk7Hpp0OU2Y zQLhaBD7Xbz@oyjLCa<7MxgTs(Uwm;;?97)vuoy?(Lv`rds_#mJY$HQAM4SvC?aa7; zkLo?ReHyZV?j+mTem`>!+eSKFdDj^J^Ot?(FP7h}ta;XvRdL~1H-=%QwPk)OZ5j(_ ULw>IE8wh6(JxbZtTQKO%f-vorF9vJVUR5_096` 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 QBjD|r-}60ewMa7k7aQGoTL1t6 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..1d70063c312dd0ef133789862b632c25e388e86b GIT binary patch literal 10196 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 MUq?=|qwu@`0^=;UX8-^I literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fi.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fi.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..773f7c997c85e327d782fb3b65e979ea12cac0d0 GIT binary patch literal 6184 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 Qr#yzz8zORDk`zP#0p>(^b literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/hr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/hr.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..ab8fe1a3ef558823319955dc447fde3844975f60 GIT binary patch literal 9778 zcmds-TW=Ic5QW=MC`dfy9}Fl25(Q9_M-&kf2tr(iZK6nd8L!u7vAt_~Z5%9xpPuA= zeOz?U?${Uvb`n`$@AUMgy6V)a>T3V}*QInheVe{WKc+_CzB~N(BDHC%mX$Qr&sg79 z(WVsAVf{tLKjT z)3Te#Y-^5I--qd@p7-^BPwTSY>-@dZ(Y$HRFD|l`g6E2mSWz<7HlJTXGIH%Yi-H_pPTN8L+{^pg%_CMc1|2*} zue4f|JliYvd$Zs%e4^oX_2=7IecE(geg9lQ_EBmVyz3Y}&HXGx+50Gu-&Ox_G!_ju zSq^sTwbAxQmTD}i=vlOZ7H+2V`svrg$4AAllIH(m+q1XRK!_NpD;fFtE#6l=X*DB) zcKX>b!%z0rg11l9A8)sBuL&g+`6qtVWW<2XYiff9@NxXsZ$Vmka;sL}!W66p#u#(G z*T=?sYk$~MuXRawPKamL4UOE>U(kP`Upym_%?Q>GzwBZfdD#yVY-rTr?HcU=KyNnm zW=E}4$u&wB7B=(=FW||oJcSvy^LuoPFXB(*{2s4d)o$1Gj>2D-H5YRKqET?7mfqZL zmTZL)?+L{sEK zYvy^xUK8&|?n4x2*X$JwHR&W#bXKC^ey@45{HNp`vxrBECF!+hRy5omYoU{5H{G0T zlry!nuQE9rqRvW*c`5d0YEF!)m#7;VF%ER+~(IVip`vX zaJY5lggNqQV1zXev@7QWZ~#cdBVcG}xP9WcJ;yfUTFtFI`(y*_Fi*tl_mc?>cF&02vs*3U<7~_0C5c?4{}fiw?d*nZ zjuUAuKmI|^K)wtAled$h1M~d_G3L z$_ON>GZgH!uHRLyNDp#bX5g$y3?#!P*EcR-NU~NrwDVS4yAnAUW<+E&pt(MGj&e+S zs@4t78kriaI9oPKh{Q(9BuiV+@NR7qL)ig#sugRLJ zIp^{03ZQGQKEuX}t~xGfexpWlK^Z)MkrPcEogKJtv!Xq*uf9|9ylf+(#TB2ENj7Wc zch!$AyYsBbir$rhlzi;DaCdx#w$6z@EE9krIR@0LY zkN9=zpIT)f^6#Y@`+f@a-1hnx=o>9fv*j&=NG>sVLR(|pb$vmsizp2d^nVX(xdRPSLkGlXL? zF=;FdjpWBZzcdp>WYO2TZj=aiBYl==_- zmeaHJu)`M*g*%uCX&>d)IVW`aZc#m4(V%Wo>KU^L9A%jM2@L`xMu)qwi1!wr%b=?va5R$uln>Hw~lMFCVn@SWt?Re zPR6&pRfT!n_;zO@o<(Gip!i#+FAKSMLpLu=B_)xaElxKea*MQXsD* z23$mMuS7i=ieE{S1>t$ z_sYFfJ*vCUY0gJr2vAwF4B}URrZH3%ut|^g9v-NxC194?>YKy+2%-jAeB2jx*NEKM zHx*Is<9?HxEcT`H;N3+$Tgo==vQ!|>`7gWR=62^T;)tBS$F23$CqFxOPQuQI*}st4 zCl@SA4BXWxp2ut<*RtB;|1_F(S7Tg1t1Ec-)K+R#%-gN59GykBsjktLweVE(NUD$p z0X#)rtbfjs+Y#tWCGQoDoMo-PTOO)zM2mvyxSySkA+*Pd(baXZoi!niaSL0ji~gS% z$E0BQI7gce;hebF=O&pEzQJ{F!+bRh*z>QdA9k}<;Z`%3Ex?$Xm(VJ)>%Q*$3TuRK z&q`n7Y)I_$Fl(@)>&AVR=^BmCCDS>bF3Vj!+}WyyJfOZ~#arrHG|#0LT`+0e9v_eD nUH|v39A^?+>Pp6#(Qr+cnCM0$Jhb*oZu`Lj);g)i!yEqryzOP| literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/hu.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/hu.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..d30ef64bda85a6608ae0484ae38d3879756e4465 GIT binary patch literal 10002 zcmd^_OK%)S6oo5GSn~rq7Lb5QB4CLS7?Kb~0)#k1NNmQh$@nq0X2vn`4`rWa7OWz% zKoM}hI=-&zo_1`9!~vm^?Vjp-+`5l*Pu=oge=ekV(ue8o^m%IZ^wHtdgEUA}wJfJK zea3oPO*?5{Ph0vs(6=@HGw)Bp9nJJ`0t}T7r zQ?C`t!A@h(W6ijqK1&N~{Zu~-X|eUb=;WbhnWO{F{WNW8Hl$e7I=}1hEZx1Zs;60A z_es{qX5#OG`moc->V;&CV`q%rO~2`xUPyRRZ*Hi6qvv7P;ksl)Cp&q*b@f7b)1y&n z$15;?roK}uINH#=CePxSM?>s~Xd@2kKg{!Q4-COtZA>-h7Mprb`q4kI zc&3rqZlGNmcWSHM(JBY|4LAn|od$>K1#T`%qAp79s0A$!^3ER1DuXOfK|HS$NQhO( z`XDcIGtyXzH$ByOklgrR#voVfsJE_>_h4N0%2_c}Q7>UH%pckprMXx@dR z{#trpd%Z5LOyyIs1Y?MO1s3p2_99{!G`N@U>r;@MxWT{hwEgS{_*i&ijq%~RT{bw7 zf7lBf$#U(G5tvgJe>d0a^WG~${orr+3YZU{M$7YO z1Z?+uyaDaO60i+?*oc9bf+6@havO!){v9y`H(-Zg-;13N&ewa8)?9&j-^>Weli563 z{-L}X9LG1oN238!uIT&IoH@}98h`6WO)=&L`0_a#5|$i4j>P8*)ITfzPp1bjbuFzm#wnfJC(wMas8ONIPY34vX z21j0(#>`$g!>ve5U44P_)O1EU-=43=Kv_ zeDy|r6Ah*QMP&RNG`1_-`J8opRDw5DMOROaP_F59Pg%#I7SUEJ3-+hLtg|6@jXPwX z`n(?A5N99oZ@J<|Smd~T3-5C^6IuKCyvO>~LCnHTO7+&Kv{GL@J<^A?eU;0;$YWR3 zEP}0*=Yv1^9Lt*`iMY9ytWS1ovQK`?)=Zll--sA%q zwONYiKZ!dOF12_xGWg9&m1Hz@R{jva=+kG(UocL1WSlmab+RMksd&U`;ERfl@f>XG zOId>&J-RIWxuZNi)Oad){e7F(dA5wasc1%frDh&18M{!$qXKWM)-%KYSM)ZbB=%Bc z=2Uj&^Ssr}L+~W5H$7w^?>4cPb+?oy!3I>_=;;%c𝔫?43E*r5Eli3STh3&lI`n zm&n9d=>w*RzOUz&a(ff|s`qp9Zu&UF`S0aMu4Z)kz*%>4eD3b<=y=Z_O50R1`#k)s zRQtHUg_nOV8QoGTVjiMtZyUrb>*NDFO6CjSq647 z%9gIZmFV_~%bvz_{r=7LMviOVM)iW(x+jT{qpr))@4o!%b_)$|Ye!KXq2dnWbfbSs z?I%?XPH!7mCi)}gPRlm_R@>$~ozr4()^Uw&ye)C!Z}sSVvMUxfJ|c&27KYiXwph6* z^4m6!x*|eO>eG4Ll&{OOWIpbK%nb|r64&ih|GW>CyXSTkz(4vjb34NCXoY%T_zJs@ zdPv+Vv0rL;zH2~M-|B-w^foGSzQzNCg?NPXj_>v0$nloGuCy`xHO;u5BcS$SGF#+S z@@Av^_fNGZw@oF=B3s?da>YGw;e6aG;sKntV3lOc*h`te#A+?URJE+;=njFA#46*t zn|ze_bMhR%4;o93vNAm0x0sPBip2OEy=#`xt@^@Iv4goJ5nINS4>&1}#Y15-2m0WL zzVDi6oc(7C*le&^v9lt$FfN_LnvihBuo%qjheRCt8aeMe2b=t=T=9E$ZF;er)^4;(wE) zGduijqG}MipqAyn)`|Z};eRxuN>*3vZmPA-G^w*vm!NJ{R}9N+XlSTju>68y=KS*# zmpZCh9fw`e|Jf2JuNLH$YuEaTuMGV9l*_a7B z#eWWP3(W}M{$hu5wqJX7FygY_>|`Iqsvxj24o!#63xc6dvfbCZ-#ZRr3MQA^#P+W6 zRp382A72*w?9=zpPtVVyJkg{owf-uEzNPzDGocu=Orwe>6s1 bk#%--ix_cQ$L6>47fzM<`f-*1h|s?QFd6z+ literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..5f6ace28d85d9b22a3d1c621a2db61f1f7cb3664 GIT binary patch literal 5868 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 P^`Nv=uR_zKGj{e5e@K8r literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..f4685edad613b0be276812f13c877ee4d34dea10 GIT binary patch literal 8552 zcmdT}-EUmg5kKT9iPS%Ur`|-+IHj8mv~LLE1gC|1?clE(i$G#KyPJ)ZyYAX+6Qm|e zRaH&WNU#X;h*S_NLP+$9uLqD2U#%+TCE=-+s;H_6;t8I>!u|cu?D(8}_ik+SVOp)6 z+>dk4otg8SncvJ={c5+n-QDSKbNw#C)m@#d6K>iq;K_`u;Fsd+xNEp&T%E%I)3~eP zFTqvn|F8SeO+4khGkCLzafxf;dByF*)wnz1bd>(fnF*epcJmn7#CSeg^mFF1#x$-g z?rz*K<9!R?<@e6`>jZaoH;4E3ojT?&_%%-99oOYbCLQHEg}W9;%|Z@pYC5M_aoOGL zcH`+i_$#tUS>Bcx`ffz)J5 zw#i?Ir{qDgueIfQtnxbEuE-}?p(>Y0mM>o&oy2-mGANYdZnqP^A}JC`{%!Bd{Bw%Yd=t?k9M{=NzYqH3ThpEZ)F)`hlXp3cNOeTp}#NwW7X+ zXY>!EpNQ7CM6qFf8c+F_KE!V=0;im_uK;}wdHLcw`C?>JwjJqfodwz&@*dFh5BXSb z$VvIxL;W)~x%BiP@K==qxr7nta|j#)t{#@BfWv^R3Gb1K`#HW%->rB_)mvvF&AgY; z5NrOm2<*{r2k?F#^4Bx+Gg4GBmp;n%XwPn+Q=bgB(=|DcFU z(~n?>zrd4TmH)slSLH?c)Y~#AU!I{y-6D_37<}r&nvWfogK|QCCl}@OCHmfBIR;-I z$XI6t`>%4E1t{BT=(gnb*LRdkEvE5^d;Q&5X0vulTw4zQ=J&%B*!Nr3u~8~JhcA!3 z@aQk4dU3MQ8fJsU?{?~LO&C>+Jc#~M#XH*P)`WOy`e>W_9jxI#zwg~%koURbj6=J} zh!~T!;|qEmC6;M{56kq%__m0 zziUa40O@^h3N~*+H^whUfg)?<_nZC_+kukqb9)OSUEm!X(VY@)U!{%C9qwzDhEoaF zX&}@7)H4{h(~ecM)PDD4$UWe9X%=$T5ydR}GR9ThV}AD-#pumrZUVoMBM$hTBTFG4 zVJswjF$A=tk%pK&dc*D~kY#@-$1Cd!I8!!Kgqo5ot+5~S0F6g#)x>QikLM*af7=)X zGDMQ8Y+qf^_B&RM`^3zto9ArpzN9`&H4)E4?w!WYa!(n#$P8(XZnnD*&%U2sW5l4} z5pT>x>VZTPeUf;j+#02_I3%+O7-fW-S(oQ-ewP`4W8Ry}0bF;j`%DNR6hkx9!8Y2YoDMC8PD5a^k@E$PBdl zYi4p~j54Atm1|?q94!sc$)CZASEMYDFa5AZl(9D3=Gk6t9lm$K>vMCR8Sm>}=5C}? zOX^;f3BD~|+ZkT7wfbsaY+^rVp<`ZK#wFI%b)SvCnZ7IX5~|~Ch$a(9EDb*{2NBK@?FPS$Tyb{p!4xB@YG+MPwUl}`*lP?knUx< zj%xo+)cV0H@yH~@+rzv#d$grAJTn*1C!?Q5N6|25GRMBZ0AJv**P6<4*oz_0`B7r) zt6j#Sjdm>A`B4@}uBi-;yQMvbChW!j5SfYDQe)F1uuj_%%k1264C9g6a=}FZv1*lj+%+hz6SE@<9hluU7_y(?Bo+GE$1*xO~Uti#vx?AG|P>PD^{qjRU*;k}e^ ztxDr+`8vnsi0e;|n{cD9obl~4WV0%82x~HaH4uAOB$X0l*~F)xFiuo*j{JnS#< zoJ0MKdCTMuYlZwCd%yxZ4ar*HkTdCLoX-=LGhX)f*I>tkGh2S9TCBT|93&=+I6BVJ zfwN`%6m7Y2{_1GHGm*HMC$OKWJ%oSB-{jxmyF5=AoZ-9yIgT^F-}=tR6#BGdI9n*= z3H!-e&NlAtp5F~+Wy<7;a-J_W*HFPcq^C`>F8N+dLw5c8_0f~kJokg~vfMF#wsRJg zmtX>`+lgx84&ggm;m5s$SZ}ST|W*V*E@2 zpL4kr1?^U~g&p0%newdu=FvKiV*mg9Xnb1tVvk3W?sv(@!_t~B#s0%{n){o1JDLyu E7Y^gv+5i9m literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..f008e1eee0dc5fc65ceb888582318fe7c6430612 GIT binary patch literal 6220 zcmc&&O=w(I6h4ctq?>|;2o-Kif5oK!>@;Fyr;4=VPa7N|i%zC#GMG%lWYV+{cNHqQ za8aR47cMNsQj(M=HkM>?=1sszv`(QgLKj)M&|Mc@6!rV=$<2N5&6}jrnh>Ua@BY2> z^PTUUbN~KpkL;5dWv?6&gQu5Pp86ys!x-t79{lC;bVdec6i@y5KZCa({0yG*_Wzt+ zUBD>kE?}mJbw)-o-Xpv5^da6yu$#Ls*k^;coP3OVw>pQ@hOsk)nSPAs@a(_A)B7l}R!;F_aj}u4bb=iZ_*YHcysu&y;89#*C<%y|E*K~c#NXt~Ue4|vISe|IZ__kWt z^uuqzxtR)#l*>bt#bElEbnX7Qk?~-;^jXbFx>hfJHO?I;uJLQH%2W6Ye=Tcma==PX z>(K^7&^m(=+U$9ZaGdMZI!@0~jL_zTSWla)h0jU>BLf)E;O8vdiZS|vmL9~Y&hiTb z*v*j)ew?Mh_^l$W$vtfeHu9Ev`o`C#{w4S$czA1W;mUO*MQiGki^2BWMxGfz z0_%Jdyl@Tns$Q)ac{JNMvsB(S54*xfEmO4DrJy~SyVW)A?P%mQ?AKBk>zQ1~ z^$y>~6>Das7h{w$$&M)?Y;1-Idq22ZacDB~;LfQ@+M*cTn@rdKC=-EoSc3K{!YYrd zf0pW{y%nD65v38ye>J+Yz)QhukcT(b_s#-D%P4+~9wXR-X`r#Pd2)6^gM;!BG#Rm)&@%>pZnQ-KV|o11JG}{3 z*awbllS2vk--zZcU9e>9;{-7wPiDo0OSlQGM(aey{^qcFSyydR~INkF%bE845led@yX_dz8Oj~b_ z_sA~HJ;dFBMJfGHR560dPLx|2Es96wDu-3FS@${_-C=m+XmpYHc!+%RIz?GNj(BGN za3#a&^EFGV^5b-ze+PjSjk54QSJxf3b6VT^ogEm}ywR93{CUpCjX7j@gN)TV%@RfA zj7x#5tN!g5U9ID*$!LAp{QIom_Rgn&V#Of~soR2zBwXo*B_d8)9k9wGqq17^*@e|A zzp9l3tAHF5-0WFwXZ6AC*bFPE zpBK`#%M(U+EI`XjdWPEVn*D7qU6YElxsfZ^cPuQeRvPuv50lL*jSj2d+BRS=$yps5 zxyI#N7r9}ZJPF;YwMYNf>aO{h)pZtLjB8HbV`q}HS?|B--dX-~(a82YV*#;z{@1Pa zH?{O#SzPD;!`qDAt@ZPe&0(ydwU$i6HoFUU$7h;EY;n`)B9Z*Jfj!6pqJu zPvc~E-I8{kKKGJ=n(;pC3hlkMcWq>S-K$k7BVfQXn(OGb24^PH+_QFi7oH;ZV?OU{t^Im1wz}m#s)!nFJS#7h2 zbam7lp?t?@m&1<&E4-25?S#+1zb}41pS;FIDt5oabA5a-yWA{pN}Ok%l;PML1`7F? zbYAD(^RUp4h5g1!r23zNkagyg_e@F3wMWDYQ^GG0{8rcuOXvA$c3ZfD}4hB@>M*larCZmHGO|s_k zh&A;s+;2BOmF(8cNAE;V)i>v(cjb5}5*dHa=6i&Gir$h&?GyJKD_CVcf5u5v8U8-i zu`RJcp67j`c8rW`@;Pg0Rwl|Lp69~29!9riR&QFkGmm%f=gs5=8|Ukv!LE-xN9<1h za{PA9=LfFlUFS%Ixx7A}6KO-%LSNtxO?mxv?Zmaq70T?_pZcyip?8rjrF)a?0(Mrp z%iTRDbtRpl`x7I_z=PTgg!>CG2PTT@)t+$e8$6ZbzzY8p5v>+Ot>AO*Ny3(Bei!%;DI#u2 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..fa4cd97dfb21df7c13065c44b70bfabaccf7ab63 GIT binary patch literal 9352 zcmdU#+iqM%5JmeHp80?ii{OB8BH%GXV2FdFBuW&=AS7PK<4YXdGuF)5CV$Eggm1_b zuvhP?bf1|szC^|b9XWF@eW|Lh+O@knfB&_VZl|x(r|J9D=rti{{-hQjUZhJHuVWh8&!aUn~{DFRvYM6HQuUqWrHng2c3S`+-&tpl8 z6`J%Q_hp3WVf&tBjp)NzLs_L*VWhVMy~i5wvZmNpJPnwg6w5!IE)r>tJWz z*ZRNB+TTwf>+072>b>C?Np{m~dNnKh$F;3TKl}4?_B`T}=qf(4mA=#)-qX%;7jbZ! zz1CiZx7llV8)lQ zOxGIRIa1Ge=|p$n(Cgd_Il_v!r+I>GVI1$v*G9S+f0CsH`D`^j?Noov*=SVc8mk}P zf~F_>LxY}HmsPGcE?SnXp*CYe;i7`@E%YKp8zZ2XIOKdbFxM^v+AG|tAW2BX6f-8ypia~pOM>$*!E*%UHCX? zxOpzy^f;II;^HS4@#L%5shuTeBL%OlSvK)n{1zM9bHgj)qO=*xUmT-#@8IvmFkiiM zDScO`+-Wbn#at1Ufoz&CgY7zGn7nU6gYz08JSHhXH#4u@S#o42~FUw_Ln&qed3U8^AYbdBgb{wk34cDiP$HFME_0eA)WUTR9iyUu-;AhUR$Dq zT7l2R^p-AHl>*al&i`0v?}dy!UAALaOZ9E*QrK2UBv@hG0(H;wrEy+Q_I}Qpv>XW)7aoau+sY4z;+lIeyQjyeBR3 z!!8>x8RKtizlYVR)x_T6hl<;AA2#|i+qJLO2UUwztq(O^#_4X`w06109(7Mw-^yON z#-MssI7z?C0HH^Xs#LkS%1rBu+^7jt88atZJm)BV*2Ju~;7jT5iaHf8)*PQ3YNYUq zVa|plS(eHlu^Lvl|4kI{Seb~070`y<8CHLg-;@)CK!sYfS-qY}7JEH@ZHBQ}tHBLu zwU%ohvrOVFot|e)$7&e`yE!K)F}qG-bFiuH>AQLc8M-|Fr&RmApTg@OX(X?paW*QF zFLE3d6ffS^?%~+=Nkc)Ry1IDXp4OJ{B7WBHfkvTK*E!7P)&(|4#B{C8sP>~VeZ10HMD_G=zEZc);ky{t%l(v^X)eN;> z#2s2k#ej$D zigSfeJpyOfvkY;%77PV>z#4cAlKCVf$Pged ztIar^(sCEveo5X`b1mXNiudd3GwCTm^H;sYpT$p?l%B{{ZGgB0>NF literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..76f3556b8dbe4137f65d525335fbb8e4e9ded6d0 GIT binary patch literal 8850 zcmdU#-E$O06vg{hUVY+ipv0i%LaC3IB|*SKKq)X?+12Q_p35Fk-_^H6%^IaGeJPwzMDaL%m#%8`Tm9*m%3MAZovt*E#*lI+?=Va= zJx|k~{+?(4+MJE`o%xn)JM;QH&#O5))aw3qvD=o`%oY;Q@=lX1!#C-Yezgpvtm&1k z4{U;F_a((Jui8joXoPX*S?8g>V~t?RspiA8?Y60XE#FYz!fpeNVPWhy)hN2R2KKa? z5j4YBP{3YuNywTTy_((F%n$FIFRU_@B*V;OZI53w zeK;8EH_vD&yiW2c^1w$U-4DfWlP))pMRh8QSbZ;#Uv3UG&dy!rHpv#kZuTBqct6u{ z6tPe3FVJu(f30DqsU(3op;=mynC9>exPbsw5=5*7@pY?4$7XU9$0zTa{M9guBYF{De(ne!%d&A zW|}e456nJmag4`;*IUxdXT6?#ahkbEe!tGy$vj)%H`C{m%Qz0IKWbW zW`y8r;O}W#)2~F%)r<~eZzJ1+Ei@CJTgTWyWjAvagf-&j<|1x8n-(lPKlLGVkOr^v zAz2q(g>BB3HM<-|V<@g@)F%wyveH23*!T&>qv) zo#}TuMcd@;4`Itmj=r-T*8(x*6L3J(rM^Ie%azX!jeQtP6Up#+FiNbl?}I?10T_mF z$FjEm?eT$YBIo)UtEG!XM&xbpzb{#yiCZ#IU7uJw=Y68Fvn})N#bhWfHmY8jdb9lOL$(lrew9@rH zw26)*?$k1xJ4dv))oFL%wr2bC5o7OKljp-|tw&ca)ZHWLuB$~dfO9mp!bR%Q=PTm! zPChBs_zdKf-N*Ta^*%KDHkfS3s;(~I6(ZZa;z9onOumgK!G%v*Cwnz#T6>8kZKhBEpU8ankuzA@x~qd zC$e)L(+Y~(E5hHLzq%3SeIu#O+xcdA;ZL&d zqZU=a)l`2@59DdiK6op=_~LndEtd6Lz51!VoKuVa7d{U3<6KS@@VV7JT>8NN+V*qL zB2F%QDW@LaK5XX5KxYC3ZM1jwR0Ug=;21xC$bkC zKFvJD-C4IP~z><6dVnOh;}IZmixfw&mD zWUS7ar|m~CfesedV~5~<(2_Ue@medd-4!m}x2U@)_heO{<<~Q(g|^@8%DL|A6xtCP zyB#e0U&}PQS!g8{zE-rN8yEUI<(%QEJPK;Se}CN)@hW@|n-E85PyEv@uJ1}fL$==XLKE8EbUtHl(gA%UeM$49uy#^)WUOo@oYiF`Z9u^QpwFW?*ebk75q(nq;c=WE0nsCBz&!H4rbv+*eW o;7NK|Whnlq!b9ytUdB%zi7MXQt?0?X<_3RqWQce>epSu?3zrwbi~s-t literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..4444f3384001cc6b929aa0aca6b15710f1c97fef GIT binary patch literal 7318 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 RdVE|>oiRJda{{D z`kCr#JME?;T}||NsJoGV8(mHF-*Fy2(^KBP(VMx(HR(XlN9mfbo~PHzqx>&38$H=c zdm1^@c%IDjoIR~E)b%Lc(fyI$ALzS$?@hjLbT>}F>V14?thxJnjfvi|E-Qs}$TiX3 zfktgf4m1s&r&{qSeIq+G>1+Kix5rR#UTP%Yz!oE2ZKc(fnf^u6)s-e){a~zjyg~9N zy~=Nx$#`rrmt08xR&S6Ay9~4fR}ou?U=(<%nA=r!5U)*+KTu#@&%g`NfT;9aawBq{EZ{Ae z;yfDXIk%Q(<5aRV87+(rWF%4C&k^S%qnz=K+DgAm8e>+S%SNB6R`M+8D%xQcFzQe; zO>~WZWJy;yB##{1(eRdDN9vTm` zjJ4l3q^~VnB>W@W6RWTm$VOa{-If<@L|V{i{7$y;kWp=v<0>!}-=hr(XEfs*MpUpF z^y7Q@5u`szwS{jisvfn^RSA@vW$CCzj@_^*(SD>~*MLJ^&2;UE#R`Yn3e=fR%^c+~ zom5jwrLerj+eBZbCK6HQ3wWl)6ZRm2HuXD^h8L;PFZ%M$^mDc(I^#!jt8X>@I?ZxU z6(6-WNKXa6D+_(PG}8+YIX@enr{rH{mSpLjt+&%>(&?V8LB7xA8RxWh1@nHBXPtXw z@w4Mh>_xfuNhG z@@=p4{?S^Z;w%w+{yIhNc4g^i1Z=6^XONfO7@b!Pz^m*kTAYdX+}OWFbqmZa{9&M3 zu33HbEo%mz^y}BPO4i4DEj&#{?N=tVK^Or!YPnC#Xx0yVy`QX9o%TMn-rY>n`AOd0 z!`0u3JzT8?*#R$r4}Y!wmb^|4VK(f^m$))d_Kd&>1%GE3p&Cn!Am1!th}DF^FYt*y=uRg zSMT257sJT=x~~~=6FY8p>zfOt4_rHr5x_j%JEiA~f4loxvkC8GTpt)0dj{|KsQqRp zbuZwb)}7Fg+=yI7veMU}Q!vUlj_;x~-D4^d{E?cqjIp2{aX|hv1IF3ib9f~>HLSo$ zIu@*r=B1+qA7T}rN4J9ekg4zE)B4`jA!@Ukl-V$=pTv3!JA|!ut3*iM|1ylOU9-FV zt+1f>xUiu4K;Re5V|681k#|-RebM_WJK;@55c^{~Q?ax8wql-K2TS};TDiM3sYZ}B z;5*iNTYdR@s>e62{dTccclL3X8@A+NFSDf+h{!nev^Im=7rO9MzV)MKff7Vr^roC^ z{fa(LpERMlZTMXJ`OJeJnPZR$_uh+W+0B)8ruE@sM|u)=w{Jin>`boH3uO%TB<#*k zy54PucJ2Dp*6CjMoh7nP%Oh`i3Hsd6U6(E&IZd0IAw*0a9~E6Mg~j`kPG6WzS{Cs? zq&nt(7g7H}zKYy}jgtfHB%aFhHTsu0t7jAq+#h9Da7tJZ*B2dUwS0x`oaC2!M$zc3 zk#@XK$Y641nNJ?JeBwL%L*t^evKrnjH`+8y?RZxToYuBmT zdo^-D$uhXQ_{^bSU-(W$f5BjTU}K>{=!c)tK6)B)vJ(9@_w$k2I7coq+UwJOB?hDR z(vb@s^(h#KG~o=>pdsfvorcaaxqe3ppp(Y0E> zrnw=Ow5DUt1cgJ#n%lil^u+&MXwrRs$9AH~zcAqVnme^0>pK>xBPnLvjEc2+Q; zyFx)nkE_vPt&(YNp;3(w%T6BXLg2#^Ipn}{z7x0=`9}m{>p+9j3G@H-aK9+fId)E1 zd{?vfrLWJZIpbr7PZ?bSiOY8MlV3MA&WwnC4k%{*U|s#}$D;7U2Rer;xmixP+R~R( zk&xJXDZT-Ul&+|8w)9x*+!#m_tcg{eYutOMvb?uSg|^pAtQr4KM*AA){`;N{j%x3$ z<+ySLtjqelI=za_tFz74CS$#0#pcCNK36T-W~&bJXri7|8Rjtq9}P1Gl<2>%$SKvy zm94Ja_Wd!MtRdE) lrbn$x^hjS}H$f(BO7`}V(Mqq}OQ@2z@{HLeUNYTpUBboZN{Pc)lZZ|%O+Qz!hX{y4g$ z)s8fCQ$4$C?dU$egVlG{ccM|-;kA0w&iA$AX=sHJwa)7re5$Eptawl?7E8sCMWdK6 z%FxvHpjap#>YC8~jJ36kmY!#ewPMQJ$27KC{8Vs`J2twh84ZnHD%!fEpZT<|*$QQ7 zsee%y?`-LfjEnto-|TlbT+uJz@3x_O*Uk?9pm-?UHq`>^Kd6Ov#yP$bsZ+HewF8Yu zYA&Ofp{tfXwQuT*G8$DI(nMAVYV}^;u&3FyZ0e#HiSw>w$%8rfGL*0C++~=slg<_E zx?j|J#|@)ZJ;jrjp{*xSo>M<3XxW@eAvY$B_`6~<0ZAm{qmjXkJxxj;tiNK&Bad}k zXGSxT8Y{47Q*Quy-m##aupeYlhDr6cHJ1CPRzvp8V*Qq$kf^_dR%W+MX6%c3&0@K- z%Cfzq(cA63Monm?DZPI|`o%eX2f1#5U6lBS*+v;23s+ZXY$>c3(Zxa^YL&^J^jHmxL!*$nTO)0)VpUJP zF~|B&&%fd2+*fVOcDjbmzBJqIAYzNmI19mGw0Qwqm+E^y3T|CH8i4ay6&3O5Kd3jI<0vVhI(6T@;uBF{vr8) z8-AB=otTcGEv0m?j$&7B`}(0VuLj_UEbfKxDqdumn0?5TJg|7_r*XT!AHLU_ADQ-S zOK&=|(&#yfb35TL(<-bKulq~b&@cJ-wrNz?#+;h16Mds*=8+(3a(`q_T3Ko@_j}RB z<9pJ7kC=nBc{GfkMQC^c`j5sUwadlv{vE~mbh#*vEqnANmQKoUu%#tyBW7PKu>+!u z`+DrfJx`Q|cB@$)Z|O;Kf69CX>qgJgHcMJDi%s!(xKi9VpWRjDO;HvORLTqCtKm9e zS7%{Jvz)PFok>@nqb?MQG59J6(iXSJd~W3MO)(I9oyn)UZO!qxR*z_D#pqM& zG458j)9V(=`s37?#jF&eU~BA{nGuKg+ePH6DW9lj2>G7AlEqKRig{~&4>`+++rb#C z+gN;7T2heq(J!y{gVL(mj4|)cI>Cl zx!~W<{JRyNaZ|i!LEelE<%xKMGu9_+85jHE$@vyua>|%RqsOqD8bj-R@knzQCCjnm zvD#>bF+1y+6&}vA+2u_`_dK@cjBLs>tDKj#4WsjMB+hjVhq{N0EMiau91ecTBBz8b z@#N#9mdpIZ!*0M3_*RnJV%N(vAfk~<*^dpp8=Wy_;dz=4* z9vxZ?C%18~KFG?*D5(OysQ8~7@;X&t)9;PW_dob3@}%gw2Kk*lZtTkoUEYiI=e+TJ zF*_yC-O6|hzV}_x5f0|{D6huUW24V?UXL~InJOSy^OCApPjxq^e*BqNq^c{FVcaro;(C>Ldp*SCLOsGJZ||%*=@2;} zOfcp;TgofQfT&VL*7`8x6`y8YXGxk#osXJd;yE|8H_xKImXh?fs&Afe z!+@PzlMNi0fA0!kYILV+iP*d6JC=rnH5yEXi7|M)i2WmLNj2Sg%KUWwXZu{*6Kkkr zcD8ynYNe_yI<*DBezT~vizx3ARhVAu^zqwE zj2Gw`c{*PPyy}3LA1`3CXUyw1gIP&r`ciOuvk z_gAG;++;LgHBKtN_A{X{d-grYtNQ7T)QO0Lv*H@-LX64}+68vxJ=BO|eV-WV(Gy&$ zNqgP8?iHpBPJh*EP!qC6YDr$1_S%1}NDX>BtmxwP{hP{?_BL;^n`dgsr^$~wKx`aJXIH? zS3SS-p6V2Nv~@Z7H@*IvU|TYt>mlg;#ig7o1db-P&15mY-@Cn-i{6i#rHm?udM=)0 zdRy`!ziHclw4okJ7BW2Dvry-S3sFIH`|~<0PtG4B*BA+PFW}r2>&5OmVgN*WSZu>#tuW4xQ?hjN1s$!&=5P_`bQO-Nkz0 cGp!1r>E{-=6_MeZZl`=jRBCUWWA`sG+M--@9A_QV{<2b4Fup>K8h!_2?k-F(3 zJ60@!|8I^@X72TM<49J5EXVgTbLKq$=b3;1Sq}HZXW^spO(=Br`Q++#D8o=swnIms zp00L6Kb-06P`{<_I{Fs6>c!t~Ts_cJ-ks~siPjZir01Qmq^nbkfMq1Sluk|Kz-V?>M@O4<$)35Yh4r}3fLjOXmyV{qp zPm*qHCsYfq=Y-QR*4;_C&=a2SNB(%%jVns6KqI{GP4*$#n-<)5^=?UT*2Q02*C%l| ze+ODSp6o%+vSo7iMXwBRMKm|7M>#4ov zgd^?dNvSVyK@MlZJ$oL%$6841(xGRl^`&EU!3hVVGSV*y8ta+q5Q%u_R!jPFi?O!Q zj??f~e-A_ht(xsGB9$XigjYkoS@>S-Qs<#f>|2C~x+*2zAY6(D=mFJOw=a(|OG9}_ z>xYw5;I*T-oR417LR;_N2`M~FYad0;xIMQ-fis_LJ-!B#++W&}61q*gGQT?!Rd(S8 z<_Edvk$6{o@kZ#Ow`uP(?@LxUnyfQDo9CZsCmfVvPhWVKPgzT`vCWmui~7l=edpx8YN>R9P-{dFFz z_;OB>%=M+W`ch>B|X)aE8{0+J_&Y6Y$&Bo>B+_C%F!j zZ9QuJp#(_^^2HAjbFpdlkkb8dKwYXmLA9@9tZK}PWUq-1FyutUxh7w zQnX%+s2IdGXVJI8jiW1%>yE~HzD~$0#LJUOuCT11Fl>3W36U#dGaQG^&r^<%lPu2rmYH})lN;o~5aY;E2S?(l!&9(-G@tRmK;pre^M zxm~H{nrv)zZjRT4g?TE{#miL9A9d?dX{8$0a!kS}Sa>&;gZjdXd1AiPYkH~@&l@Rw zt?8-dr0H0j=y@!e&QD7jm$SST+nmNaInO`S6@bSPXu5W8#ve$l_8sIAav(l)7;o*?&!VOGvn0cVtAWCpfEn6xu?E9$j)>E<$JKo%G3M z4AWV&X5j8FHDfgfT(+f+x?YIf*ryva`yw<0*2mG>t1au$TswBsrB0$Qyi1*+g%l_1 zI$1PUpkIv>c+3A2Eyv=^dX(AzLl?<>Xs0on;0?szbD^MW|EBJ0Uhl|r#LXfDS%Ns^ z^CP#mR~K}W+gYUoN3(JyRhZ4F@cgEF+4fphkGCDgbw2sBekWD0P0=`Ywag-j#m?zG zK4U$)`pT9~L}q1UBxVZG)Hhnrvw6i^#SE+noj3!E^D8>aMO}J3YC!LB zwuKA!GK=tf_1mnf;M#M*x~#^`5BsGW=Xc>#Gaz-nomTVOqS8Y(@0{g-qssYAQ<&B` zW?YIqLJZ{ve9}1^(iO7+BH^KU-Per4Z8yv7KQ3Gr)8H?X-DadraIB`r23AkoG|B3# zU7f)cH03&rY)k^sGMHs%!y4kdYyD|_k|w(@V=h}=GAahDbh6&XH8Pdq_GNk&`{{bF zd!Ixvr3KGcSPE2IW@@(7v{bXPz9-Ey9c#k;cOgf;-8t@mkiAlN0;x80dnxYnNR|-C z4$m+abA7Vx(=N-n`5gJS@m7^;9<=3BE!sBtG=9@$n~jQX?9Oa+U34Sb&$9Stg{s}T z#eWVPS^Tg&e}~npT~1Y#2?bWq6TNLt(^J<-KB6i|-;NOPRU84!nmaL#_W#p(R0nx< z@I-!_M-Yq1e@uR<8ZA1e=$?DpP`!L{H3eiRd7J(GpMf4>9vaLP*lAOLJGnlaPI-vc zWX(|}9&f6(x!0OG*19I0XnZH#(9^GaKhbV>z0S{#-iB#k^RarU)279#Ct5W~uQXFAAL+!i^le(x=r{UZ%++4LYfI;HI&5}^)rpY|5)oS6*Tu!Jdec}T4}Pp(D%B=Kn8jrYt*ywe5l=wwEATh^l%r{v*&K5 zu}!TRi#t{y=JRX)!tG(6AE#AG4}FbA;aK;Ng;QiX%=~~TXxPnXu!^UVWMr=4@O(zg zzQzW6M|a4AcWz<-k;bR9o!mFlyY1jqD?;BO=XR!LSEFc~5hKLU)2wMuW;bWX^Pwn1 zRv^KWhKE(*k+n#Dtn=D*C(9|c`$8u`&yQM*^_g4WHPDWuEa@UU&m7BQ*tMZ~5IyYZ z0ZqTuUhI?ekj1>c*&Yi9d?xEyAHLvNJJB^rZ8MEsEhDAHd?^0{Ccf@V0#ixA@g;mE-hLg*6?w|O1#1_M;BxXU7>@;k=gzT%XAzkAL!=VPG1Mx%JM> z&*xkowFMm)$zQLYr*0(aTL!o*zOG=EScR8F?~Vf4Uq3F5g&JFCIipw(c@?v+2+H0K zjXF}z``>b6c$azqaHFDlnf;tHn>kjH zcY>W=*EQC?kii;2nGpm48qJ*b`Y2N4kZa-88{7Sr0xT zPV8oj{hA0Fk_UoHazOIJRb@Hk?w6G-F_Y>LiS(^4%Xt0$#dkCKZssBsoO0)2TX_@2=yQ zD@(d?_PaGD^ri;7HBcD!ZEa^$*%v?MwnyTVh`&5mI{)~ziMuSn=i1}GGrdUowO(Cy zpfU1Ktn{Vi;7T`SZV!7b%Rt5;AB3=m93W(VKaZ4ddGJ;9)91#L2SxN~mh)tPdi^{- z$i0Vtq<$MmPsPjm%nQ~Lw?;+uZWnVEQlCfqt|U+YZV}=irW*5p3)DZ7WJ{F9SNRLZ zQ|Y_*hXrMLa{Lv$+&>se)3rYWF?8n1?y<=0Mq8Jsi(oBys-yQ+Xax(#VisxTaYrkD ztGiyyb&d3w>H10MRKJC;e@>4pZ}E$?gspGOc0)sJin;v?uq(}i1MKs1-+Rg}&y-!3 z?_Uw;Kp(vh;+JbP{1K6q{yBNCy`p>b#Gu;Rrs{e$Z~CU}1efgR+s*6jpWb~2-4}MA z)V;o#lI<-#>mAk(g`saw=xID{(82S^_hhDvRv#F$&m#Wxj5rOlg_okiSg%- K(m10SZ}ATVyDMn` literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..e7c70db714563b43ef24775da8e621f0e8d04728 GIT binary patch literal 7834 zcmc(k+iqJ$6o%)PUh@EML@6NB3b;gw2ATqDE2@@&khpOUPAofi6x#`T-}V)H3H;yu zUY*(7JFSD-M3$4i=dflS{&kq;fB)G?chXnslk_0<^z`-e=_vKnOe4GLK%a@8_R}<- z>uIe2eZ3v%+tbq|{~zYnry6DMLNjx%>!pRp57KQt{h{}TcC+guKlk)DOe4+5>Y=F3 zw6m|7u||h_E@wb}tht3&?WH5l6wW82c%Hsb8yfvi-^-;s)$CDvnU3>0^UU{jN$+K1 zc9c)ysjoG`!I}0TYd=f#yLdB?`{D;qYt9cur>{}8#Qvaf*-rDxj2>&QmSK@4_%_|r zrq+KiuDAX4rA8QMoi!f1JJ$$yoN7IGwABXU&!@v>SZz~d*cad$Br~J zmvkE`Y_g+Sw7x&BvkxboKS1_&N2cKha64zgymCxSUJy}qiJgrB%kXGKiD2S%(M?Q zB0bhdroJqWm1`{&>y&tB{F%$ZBQ%g4ug7})g*phBXq>Yd5A7Z12yv+MkPi(v`MPe2 z{}PvBpt20yQmbhA2qpOSr(GoGchm)}ocNCyw4 zt(mk%v~gT8wy^o%8Fd3`7Vm$NcJwJxZ7U;=DBlxC!7Hc<{ivh`~IFk-D?PND+12`f$%^*{sh6IsCuRSXCqa#O9KyVW}Cp2lv*#>7SR4myuB z_n-vqfgI2a-;Tvi)P^TG788ee-&uQ^&ebtn5V1}L`Tk{!%FjC)T^7{>D?8brl{+-ca2DddPU~oKjPaXtJ99ZdW{RXM0a_mC(#jjwI%e z$_wAJ4|jVnR6{#gT0-#le%8nUP?013=xI% zPO`S9k`+V@^Z&qI$>YXTH%CZGjcjV%YkV@Rsb1iLI@gtWvu-Oj5c;mGxUHabf&uIN6zmRxaAi|)xvZ5?4N?!(-1u*Qc+TH%KbfIqlyG~VE6 zwB&}{(}&j&=a=Xa+;ml3*P)#zH>DnMm0}&;ES^rXe{)~5-MTbI2B7z|pZz;-kn6@G z?!9B7pdE|4<(_FR-4%L4&0FDB;|cMo?h%EAabMw1x?5E@#GuPEE0vpX#qPPR=2h?Y zZ2-SK*6F3Y%E!j zy=Ihmv4fl!`7-jpJ2TyxAGu#HdFUxyywW?F(AB)VH%+rHC%AW& zn)rvRcHud)_K)J2J|OlkwbYg*-IY9aIl9;O8h5qsotI_Bnj(>JFUL8{9x{ON$fh&- z4xe($6Ya9)s5l0Cw)VUI0tP%T+ouAhYmcT~Yrw=~x;$#%Y}xAW%PqRi4>1CFF+Mw#Uu=KC#aTO)}7IRp;d0T4gq*$Mo z@4o$n%{SD!I+6zBSDBJ)_!WUz65R|U4Vj0|GL@zyUy0kbN6pj;=)LJWvXgwoetwI% z$dTvWG{|J^T;!cTx!wCN87A`Hq7iFfC{BX@A{TcK_rBMQz3|=C1X*~VC48tEek&^S zjWy$&*wxPoY4L05pxgN6BlzE3X7>x}rM`3RR2iq2wzbdq*!mXDnv$2IVxQ$2&-d*> zTcH--N#~+qtWC)g(rbc7={b+ZOF?hxMMkWAdmq1aOX?hI%}37=`sCZX>S=fAyQ&}8 z;qYDS(wyJTTs!V>${JJkQzNI>S~Vx`JSn1$KPE;h%K!iX literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..058b4ba6af90d135e5944ef693172f378e124d40 GIT binary patch literal 8492 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 WgZRhO=V{bNJhg92Os+>~yZ-=ND90`U literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..ffc576725441f59faccf0a4a30980913394b3525 GIT binary patch literal 9716 zcmeI2Yj0ac5Qf)Z`4gN*M5Pr;8u&(q+LV?Gm76F5RYHv%$4M<;V)>Gg{NetI2zZ~J z47oKrQ?Tis{e-m zyZSXlFN}2G4+A|p3$57mNKX#Kh5mVeqNhDwHPvQY&+dnv@R5ESVN;`i5+3W>X88R2 z>P@JJVeC(jp018G^0}@$I@kXhzpf|D(~7MJYRfz39I0Iq#(LficXaie?#Jp)>x+0@ z=&lvodhe~FaHyVjz3J#_E3V4tVDygOjn%3V-spXzyr&V*!y~Px2w&-_UKQgXY1DxZ zC~2wXS)9KeX=C(W*sXPSE;K_uT3>~oiPP5SB3y~?j(WoTw%W4(k*){2?#4Uj zYN`JtJ=xcaPvQ)9y=CkRwdFk$hP#J)dQfBjk!IxF(9w=}$ZI1!jM7>UE}*3s?nY@G zielv27wu5n)cLcxf@Y-IB<`|1R~vA2s`jju)etX=(!YpQ4E5iMD?An7!Pq#`aHw`j zwH;>~ix$?6%<38kSsm&B;~HbP;>f%^5kf#Ihy^FC9ISw!B+U&W1N|t%H+l{>4MCxZH{smgjD$^k?dg4AUvULo?hy(}9hu%QAqUoa>3t$zFr4GofG{$1{02 zuL_NPB`#k@>~fVOaP!*ozc0Ff*XlnFuO!W}R*JNdZWV6^YSGgV1e{cG1xmNWy-HnQ zo)bpj>zYxA4q|mS!xz_{F!2tJzS6t0ufUffJF^&|4+>Xptqtiu5Ig zkBAKH0{Zq+mS=@e&APxl*QPHsq}-F_i%^ad&_ldT zjv=V>C}ff7p^T+fNw^n1$Uqh&`4ud2A*+SwAo_x@({UkOu-J0d^2bhMUqEg*;v7o@ zF0nn>S?D&~Tt+l8L_#}u`EYJ5S;k9l6FeSpyd>#QAcn~oO11vp_vb|!kq&l4G$ z&m=lT8qf;D@R7&*C(1D^#+uM?w26p-XqLW2n~cVo@TU`{JTHoM9J*8|wT;*&kHXBK zIMSI9AMc~A-ZP(y1K9P)B`Q7h*@^Eg`&kE#dz#;3g6U`=Q4SV}?xy2|JEc{@`>Gg> z{=~i@BSi-u;U=6y1DeFBtM+6YifV7e3*w1#&dK^x7P%N_(A9Zd!S|=kuP3c(MlO~! z#r1Fu+KKha|MBnSM=8d#44pP`iS9r7Jh{xO)Oj=!+U{$2{?@Nr?u;Nxf;k>MjK0L`Z z0}(b@Yv?>F7py5Yt5%F_-$gPHqIY*0n1(R=f$CFeG~VH@j^$erX&o@FC>Jp6unnC4qYOxY9C7WcDYlUT<08XS*ad$Wpn$|}jzX2tJh z%(kClwe#ud-TGRE2H#=vkyk^kE|zvXa@0FL)$zXJ{dWG~p;kiG!}cF$<)(|w>lp@B zXth%5d^sSzl+qKBw;HY>b|HY`^Z6J*$kTZ@(_v zJ7TvMtG1lPW?K|$XzYwtTbAb`w=H(JdW?Llb*GrutUGbn&(e+D$0htz{U%Gc8vIRm z+_jqT2+H#;k3?#z_`Hse*(jVX6SSQj;%b?^D9Oa`a+%B?7q2{9*58-LwL2RZ-u$Q? zg~Z=qgcxcoT|G-}kVmOnx)hx?%jH;Z+=$)@T)&>c|eEYxvgrV^E=|=Q#GXnJ4$6Q^Z4SVhE}|i3@2x z^J<+Z74;{x=R_JgC#Zs#E9_*_P-=B`DA}#?O-GgIBe&oy4)*%NKN%l7#&e!dM`^yY zD~(T$%wr|a<%kIz!zg6_mLIWW4nGot_A?({=&K>G2j(@$8XM*;y`@x=B&P&YbD)>2v?i>3{wn3d7-c_%1X;sk^c6-P^Dh_C;9_oBG?)-K(%0 zj&!%B-!(mL>MC`&6Mx%rbVpR)z1N!qjVr^U=$ql1?q2HoP_vo!KHislYKK4d-bc5! z+P-G4>CKj?ZQZADu=QD9nW+QD=1Z=W3!DEyi@s6!ZFjuehtb z$>M%dhE-j6idoTTHNt4)x-SDy4|LbiZ!^xD(X6Eco-W2!MvG<9@5Zq{!d98r99w%% z?;E;$cQC1$jB!rjU@BbDU(Ufsl=F=!4K#vW_aw%U2h)1Gr}Zag zD~z3u^RN`SL>jKcm1zHEcq~cbDVtHQ<{KN5-CmTZYv29gK-P_xn|i+|X}2rVMxU=W z7hhsMIIR1I`A*Wbt;smXyWbvkHM$nwh{hhPx-_eumQJzkRb7kXH|@8jr@1Ke2T`_Y z6!{}XBt9Ll$*6^se87AsNq9n1#fFmiG^0+@VoTm?|#1v?0iK^ zDXuPyKAXjnMa_6F+L&fFL|;`bd0w%g&&|&t()kbJciGcnvv)!BT-U>g7$A^-}zNFC+*L%nD4nN%te?^k75ZONZRjh4KZxLk(IrRCEy5Enda4hNThJ)@qKFJBffwH|LR0;Ie- zSiSAUNZe6|OxY0hQYxwtfk7!qtB#-0!GEJw;ux76d)$uJi(O!C!~o(T`?lCwwNr9^ zvSH%p`k9!^*7t!>S-B@WZ5#rJ++pvLJ?e#kt9dra&81 zE35i_sPT1SOvbDiceL}7e6Y7^j5Xbi`So2vg;XUA^M(_CBb9XRk99BJxAJHL-ly!c8uoU ztFr2coM^c^uanOk$4{z&K?>$uzB-&CQMyXVHsNURPo#c^XNi*|oUd92D(LaJY8{>T zM*%4Z7_v@wL?sAX^a^NQ)dqDD{Rv05^BL~Am|>xotdlMt9leBuE-9*vE1!|ute28< zc|N18R9dIa@8^m6n)F#V)Pr=(a$fRCvM=x8FMa2#BuF=pK1D-0gbE%lEs262(H4D@ zUSIgW^lzIimn8-3f6`5}${VC?7@VqMH^%&qWK7TPNEDa9MZp7EKk~-9=`S&c$mjBZ z7%8c1>wMX*6ILW)Dt;k=M7c%GIxp@?8?%q6k8ZTunlcB4#d2uPz@u*t)8$Hu2 z>u2o?A-DI-MQ-s=v{K4ksRQ>S`atSxCo8*D$u8b0rr!xpJFTBQXe+Dob{-q6@-`(4 z`&hl;!}v%)y*#bx<*Q6DXT>yfz)9MER$2AyY}KK#6rN`B!k)Z}LgUp_lB)cY1zeW( z_B)a2U-`J!ht!LDD@piiON#zj8NFw(Ux+&7L@G28rp3Y((R0t*SEXMb_X(M~qi5!G z#_&Eyp|jVZkpvo@BAyy4cpOsz(VVK(6;%|DLiHPj((~S1XdQn_3$RT3S`V+P+@> zj(caK2I=9;#%&GCWe|u=+NysIO`Vc`aH0QeN4w9Ct3=d#U}CC4L*Z z0OOjc+FQ5i)IHvcSWqtM4uT^cC{Klm-2OK2wQ6EH}C6HSEimmr-l9ni#T+L z!Af-DtaE7HS)L4cIuaQQ_2^)j7f;3q+d>B*0MKueI46750B!BNx0B($5Yq~Mk=)ZS zgfAkV;H`HAS*^DRmauo~buw-xyab=<1?)a*6GC^SL7{r>W62VPH&4g LC2i7;aJ=y^OfO+3 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..71cf325f2d772b99f607beba2e97e43ebc119b56 GIT binary patch literal 6128 zcmcgwUrbw77(ZgNxKVx7uxNH6&edh5OpS>eqv+;l1{d3cO#>^m-EG$Z1zUP6B!eJC zA?RRofBiCLQf;~e~R85`dRc;kpJi9 z>LQI|ZjxpuXq_d>H13F2dWzF~nRa8>qqtVyt*I28YXPfN{%b)hI54*{Y*mZlRCs=Oks1Ds4nq6Ei z1>=kTKP;H1KGl1u)+SyP2k6f{F(dUpBBg_cpyxPgmZA~ZZDAj^Z^Z5{J(+&V-gLTq3NuX|TIW9LKTEtrC&SZ@l!JYn zp>K`L?`uEs%mjG{R_Ubqagr|Ykq-K2X)myV^I)xd-?HzuW%x|2!+M3gi7gAHM2^NF zqpADd$!b*VZHbQP=gDBXd1^WNiL+_Hl5mrsMb?*u-8yXQ93h|a*OgxQ;wVY%O09Sf z-(qbU&G66O=(}5O#mb$C);3VvH{eF@PTrb*+EQ9SV+Cq%G8q4Q<>yP6FReP^-fA>Z zd-;PWdSM6C21;{FdafW^EJR`>tOMCunuAqZEcmu4BeWpBk)wCN59{&jkB-J%18bY! z{V4HOE*M8d1GPZ&xncM=P-}?-^?&zkYgpoUnnxUXwPe=Azsz76lD#O=2=8+2DKsNm zEA#_HSrQ9Hdgh3Ok6ejBFjSg$Iv!cIJvGOMv$4VM=)5a(tIp}-vE0LyY=F=k6bZ6u znREeLTwAgZh99r z!rF8-b0-2%)*}?v!t7!&9+~-V>VSPc*IX$@F52LgdG~hqVB*i7u_4pDs2X4^Ke~|j z%~}O(xH{3fmv@dycEz)~UR7*i+NT&r@9d;eW~+Lh(&s78t7ooO<6F#4^De%_Qm#$^ z8hbSPU%v+Ld22+VQdfODeH%3!YP64S^Xu?2I0-crA_TRNW-`>9_yX4$s2B3I+LdF- zPF}<`j*xa*_nwS1RBv3ba^w|8DL|>{RTVX&__Mxf59}wS2L$6sY~R z^!QYL-tClJTIlG#}(v@sHGcgXnD}}S`GI@pnZ40sTyiYxne7pTS`Pi-IS<0GEw1Ds+1?-= zd|`HdNvM59rq!}ac9YuY(4YwUe35c5b zEqZ2HQfHAXv4zpbYb;NN;oSjGo4e5IWvGi+eo^w7xGxc2@TX5gX8r#Bb%~qjEOj31 z+)L8(tpIvj&i1G;IMbsx;ZCebwm|1wp%G;#j>8GU1MC8Sqie+)#GbO#2|1$gGL-D5 z%#9=l3qt>gFY!rP1^QX?e)59z8p*4yg_{&Fs&e#>PP!4gP6-oV{H750E}f(~PX2^-Rn}Zvq10Ik9)L98RPdq16>1lmgP?Va{ZlzmKfH>YLl;U zNBHfM$?d^7ZcjFy8?)7@VzSfrA?g|iZP0HFdY4`Ug{Mmg2}3!^7_(iIfmAHdoT7{?#FzH}4)F2xs|JZ3&JSbi&Ap=oA@i zd<%d#&f_XKwTXIka}5Ec0%Lrqg^tl8jH>$t+|Hs-%9Dit&O!AaY&rBHCXx?`{W2S2 uR&}j7&EFU5T1NNdSl34p+GXT0+(&njbgD1zrc;qA{aFhgD*95zSpNcBsjdP5 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..b9517885bef4d31d2b9872d18aef01cad078a310 GIT binary patch literal 5748 zcmc&&OHW%z6dsT&NwV46zZRDD466bTRZXez(MnlVihI5}9A>U(~K&dfcJ z?|kQ+DLwY6W9m)yn)*OFba!g|ZcK$#hDO3FO1~uCjjEK&)7>Qfhv+FvmqT|+{h!dY z(=>{AbM$70<~b@$<5AT@cLVgCrPWw9r|%tlN~j5XFS8T0TZUGK=*=XJCg|RL1G`Vs zyDZI$s4;qDBu~v>TJsZHKG+DFPLpmS8i8d_(g?;ek8L2@ z zspX4{|HRg!^&Q3ze(hC9ou#kA|4}U)dpttlpVl&o1;x8Fn}IJc#|o;aWx}?|vF-Rl6mcv>$nR;bL6W?} zJ`@YU>v_6>bw}m2z8P8v&q0fvmKs z_E>~`njrj2qygPgHLE=d{31$y%1=MT2c6oBX+0;eqX2#uG}z1`=qYDgg9L|U@ouvb zbGE@8vtTs@wW+op)&;i%bBF=hyEbkvmq_@mHs%GtZjEgEHhi}OO~ks4(sTXZ7^Gl} z*LM_y%*0^nU;T!857+)l%|mJ8Si`5tNDjp;_y?H>T36;o*z*7J4p;)Y0JxgcD1?83 z8bl>n268hqx$(xFBUl70h4;*(ywB64O(Lr{#O=RMCXXX#$`OrBY}XH%!xe+ce9ZGH zLI_8XJzuIQuYSjslv&wHqs;r2jBoan_^-?XEtYIsBaOuL;Q56d%i$>0G>Drzt6q7! zZ%Wl+syBB%W=l~1^{tUx$?y75LAk#VJR+N;x&bqyN->!MwIII1nI2g?L9=su4Ee#0 zRLo?CwIdTT*Qfavbrn!wnf*~8ab=cv<O4spGgWG8qZ+x*IDZ+j1Ys9abeL@>;IFfXOKx|I%G@ zwF!P?X103(sp!pH+X79cv&5w>6U87pA@`hy;Y}ZUM`l1%XLF5We1Iw!c~@$}Fx6kE z6w&8MjnHLrI_@PueMp`~kBffSWS9-#;#BoHj;q>quYjNJRp`MY)OCwzL_Qn$8A1zw zW~azR>%b4=e`?f#V>UgqC+mHMB<4;YT_|?|s1LaEK&_P0%#bE~pjXY)h}eo_afa9) zwt?5tQ(_KyL~M0hkC^iZk>8fPn$$Kk9=_CPbZ1b`ntRF294PEjF16SVy_6ecae79_ zTMcXHl#Moi61-iYoeU$IKDu8c(44p!Jb>LAZb%dzHWu9S)`|q1k!!apA_a)8X47t$ zP5H_0742aYorUoc_?9y?kBk4!Da)j0S@F2(=>>C7K4UT+hi$215S;~1 hk?h{W)|tP5Vw>UN?>3E)Hezh9ux;&wz4m^h|9@yEQrrLl literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/_CodeSignature/CodeResources b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/_CodeSignature/CodeResources new file mode 100644 index 00000000..bb4125f0 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/_CodeSignature/CodeResources @@ -0,0 +1,860 @@ + + + + + files + + Resources/AppIcon.icns + + 4McwRDEss5BzWwUMG2Xf93+ze08= + + Resources/SUStatus.nib + + ECVWRExfxyDt5uvKRD+70wc9J6s= + + Resources/ar.lproj/Sparkle.strings + + hash + + Rf4jjdgTqvfw5JO/6f9jHMURv/U= + + optional + + + Resources/ca.lproj/Sparkle.strings + + hash + + wGGx+QzPg/20zZTq7jwCTgf/Ubc= + + optional + + + Resources/cs.lproj/Sparkle.strings + + hash + + bY3rkqi/NJtXtjpK3FbV2o0gxbQ= + + optional + + + Resources/da.lproj/Sparkle.strings + + hash + + 0t7SuLDMBZVsY240PAEsVfH/1qw= + + optional + + + Resources/de.lproj/Sparkle.strings + + hash + + fsC7FJvExHE/2681tuUrjkSF2+A= + + optional + + + Resources/el.lproj/Sparkle.strings + + hash + + NbIN+TRHORCL5Gfj68VRq4KdPXo= + + optional + + + Resources/en.lproj/Sparkle.strings + + hash + + cHZov5FaqzfNhnBo0XdRuTMT4SY= + + optional + + + Resources/es.lproj/Sparkle.strings + + hash + + QPG88BN+x/l2Qk1NLLe3wRa26mQ= + + optional + + + Resources/fi.lproj/Sparkle.strings + + hash + + yd6pIoSj19HMDIUos4Td1Fch7bs= + + optional + + + Resources/fr.lproj/Sparkle.strings + + hash + + X3URilwJPVqMTGbtrYdorODwrMA= + + optional + + + Resources/he.lproj/Sparkle.strings + + hash + + U2WmlYGYmeeIlSW66R8awwmNXIE= + + optional + + + Resources/hr.lproj/Sparkle.strings + + hash + + 7LLOVs76ioMwEDV8Gah+6sV/5No= + + optional + + + Resources/hu.lproj/Sparkle.strings + + hash + + bNEmsO2LyUsMjTESH1I42V9sAOo= + + optional + + + Resources/is.lproj/Sparkle.strings + + hash + + 8fxzD9ZhrvIZVZB1+QSJaPzg80M= + + optional + + + Resources/it.lproj/Sparkle.strings + + hash + + bk1J6vpZjWeUFhBYWuWZf8TDv1A= + + optional + + + Resources/ja.lproj/Sparkle.strings + + hash + + f4EbR/GfMsKeWJ5DN/vhwg/lUoE= + + optional + + + Resources/ko.lproj/Sparkle.strings + + hash + + FRHRQPCWEk9GdJawYTuccg+E2tA= + + optional + + + Resources/nb.lproj/Sparkle.strings + + hash + + sgrDElwUxXtzdw8WaUFWyK3pG9Y= + + optional + + + Resources/nl.lproj/Sparkle.strings + + hash + + PWbC08zHFLROqivY2MAklDh6gkA= + + optional + + + Resources/pl.lproj/Sparkle.strings + + hash + + o7deBXE2Ct8/vQxouej5KkwTcUA= + + optional + + + Resources/pt_BR.lproj/Sparkle.strings + + hash + + /adUv04OXQkCFv+Oed6qktFVQ3E= + + optional + + + Resources/pt_PT.lproj/Sparkle.strings + + hash + + Mji9loJOJvuDY9hz3FhQ4H+HY5E= + + optional + + + Resources/ro.lproj/Sparkle.strings + + hash + + 9U+OTz29kXKZHY/nmvbtemMsB3g= + + optional + + + Resources/ru.lproj/Sparkle.strings + + hash + + VpSLGNvZ6sbRYsF23L8m6TG+P6E= + + optional + + + Resources/sk.lproj/Sparkle.strings + + hash + + qn/mo2EFOyw6keezS64Wo5ZGZXU= + + optional + + + Resources/sl.lproj/Sparkle.strings + + hash + + kwvdisufBenuQzrVg8tYKTX+qgg= + + optional + + + Resources/sv.lproj/Sparkle.strings + + hash + + 98/sk+A2Ew1fmKpuKZ3rq8eS1EM= + + optional + + + Resources/th.lproj/Sparkle.strings + + hash + + HQwGW1Ebf0i+Bl4synks3x2SY2M= + + optional + + + Resources/tr.lproj/Sparkle.strings + + hash + + whUQco5F2wcYdjc+cPKlk+mtx7Q= + + optional + + + Resources/uk.lproj/Sparkle.strings + + hash + + JXhpqvLkX0yDWjbWgsk2wbSObKU= + + optional + + + Resources/zh_CN.lproj/Sparkle.strings + + hash + + OnR96Z9tB0noODRSYssSs63+zGA= + + optional + + + Resources/zh_TW.lproj/Sparkle.strings + + hash + + 1FLKoM5jZ8JGBG/nmyEIA+/aalA= + + optional + + + + files2 + + MacOS/fileop + + cdhash + + JwkAFJqL9xY1mTI+1Kki3oSFsik= + + requirement + cdhash H"d5bc45cc18a448c02d5c4dd6859a64524a5b8a85" or cdhash H"270900149a8bf7163599323ed4a922de8485b229" or cdhash H"1b27242b81a5a51561703e2bb8a5e01acac436e9" or cdhash H"ead0c4c63eafc5d32327f0dbf958b7bd0993ec75" + + Resources/AppIcon.icns + + hash + + 4McwRDEss5BzWwUMG2Xf93+ze08= + + hash2 + + nq7j0ugQwyNbJn/7zGFwxIR0njwU3i7hAYKEyZhvUfE= + + + Resources/SUStatus.nib + + hash + + ECVWRExfxyDt5uvKRD+70wc9J6s= + + hash2 + + AtY9YmPv7cUlbFWP2vCyVdi3/M+XQn98wOlrIES2Dgk= + + + Resources/ar.lproj/Sparkle.strings + + hash + + Rf4jjdgTqvfw5JO/6f9jHMURv/U= + + hash2 + + 2cAJJ5NTxwpRgp24Ca3EuTXfaIIzsYdH3Y9cNCalZfc= + + optional + + + Resources/ca.lproj/Sparkle.strings + + hash + + wGGx+QzPg/20zZTq7jwCTgf/Ubc= + + hash2 + + om5I6jKleuRoCwjfrRRqKWQbs2l8lLj8QGKS47cxybA= + + optional + + + Resources/cs.lproj/Sparkle.strings + + hash + + bY3rkqi/NJtXtjpK3FbV2o0gxbQ= + + hash2 + + RfJgT2b3STcLu71+1iU9ZcSXbfwMWG1EE1C7Wrf3xBk= + + optional + + + Resources/da.lproj/Sparkle.strings + + hash + + 0t7SuLDMBZVsY240PAEsVfH/1qw= + + hash2 + + wu0CpGqE79+TXKIQm+q7ycPTuXhOlwRr/wD5uGHJzLM= + + optional + + + Resources/de.lproj/Sparkle.strings + + hash + + fsC7FJvExHE/2681tuUrjkSF2+A= + + hash2 + + XUpgsFH8KmcbgggpdYbJScCg0tBic9tNLdFh+8cbPyw= + + optional + + + Resources/el.lproj/Sparkle.strings + + hash + + NbIN+TRHORCL5Gfj68VRq4KdPXo= + + hash2 + + wt+2xyusmWAQuJ5kAQlRlvFb1wO4L7/rFdG+VmNjl+Y= + + optional + + + Resources/en.lproj/Sparkle.strings + + hash + + cHZov5FaqzfNhnBo0XdRuTMT4SY= + + hash2 + + 39CdfZZ1CQQz1Gd1+Ukxo2JHl0XESoc/cqWKF091WUk= + + optional + + + Resources/es.lproj/Sparkle.strings + + hash + + QPG88BN+x/l2Qk1NLLe3wRa26mQ= + + hash2 + + mtOoKdoTpGzeTNyzxkVGOMsE0Z3ZZOsmIKDfgA9aj8c= + + optional + + + Resources/fi.lproj/Sparkle.strings + + hash + + yd6pIoSj19HMDIUos4Td1Fch7bs= + + hash2 + + +AiiKWEdH3lesozLJBn3tfK6vi/VSI1/TnWVmIdVVsc= + + optional + + + Resources/fr.lproj/Sparkle.strings + + hash + + X3URilwJPVqMTGbtrYdorODwrMA= + + hash2 + + fyqJl0MhXYRILalxRHpv/JorWLOVLPtNcJioiPtlnYg= + + optional + + + Resources/he.lproj/Sparkle.strings + + hash + + U2WmlYGYmeeIlSW66R8awwmNXIE= + + hash2 + + 4gUlWkwTANV/jd7n4OZoXyT8CAcgWVk/tI3a25wmuLg= + + optional + + + Resources/hr.lproj/Sparkle.strings + + hash + + 7LLOVs76ioMwEDV8Gah+6sV/5No= + + hash2 + + TwklhrooHTXgV6Q9fbvvAB3mPIh7qDbEsNtUzo2fQuU= + + optional + + + Resources/hu.lproj/Sparkle.strings + + hash + + bNEmsO2LyUsMjTESH1I42V9sAOo= + + hash2 + + sRkp8c3Bx1qWdhhSNdOap1PbfmiTziINy1HxGea3SWU= + + optional + + + Resources/is.lproj/Sparkle.strings + + hash + + 8fxzD9ZhrvIZVZB1+QSJaPzg80M= + + hash2 + + xcV1yh/zU3U3TsRUT6vGybvIQitf+ThrogN/uOWmD8k= + + optional + + + Resources/it.lproj/Sparkle.strings + + hash + + bk1J6vpZjWeUFhBYWuWZf8TDv1A= + + hash2 + + Y+caNW+g0mt7HP4JrBxJw+uDwN3j19UYb+q5r9ch4Ow= + + optional + + + Resources/ja.lproj/Sparkle.strings + + hash + + f4EbR/GfMsKeWJ5DN/vhwg/lUoE= + + hash2 + + dSPIvpFbelHRv8liJjN3TUVPbgD1DfhVSGmE+S99quI= + + optional + + + Resources/ko.lproj/Sparkle.strings + + hash + + FRHRQPCWEk9GdJawYTuccg+E2tA= + + hash2 + + +bxn0NPgkxdHLa1MHRT+JRlYmy1jpIuaenpst5RT+RA= + + optional + + + Resources/nb.lproj/Sparkle.strings + + hash + + sgrDElwUxXtzdw8WaUFWyK3pG9Y= + + hash2 + + FG+w+OnLI7nwnNCWiMT50LU98VWj1d08ElfX4k7Ok4w= + + optional + + + Resources/nl.lproj/Sparkle.strings + + hash + + PWbC08zHFLROqivY2MAklDh6gkA= + + hash2 + + xnQkqxaO8zP1xpjY3nyjOd4Fe0gJon2Dbt456ukd/Gw= + + optional + + + Resources/pl.lproj/Sparkle.strings + + hash + + o7deBXE2Ct8/vQxouej5KkwTcUA= + + hash2 + + pDq+41jhfESgJauedrYncFY1O5EMEU3nRyl7mmyYj+s= + + optional + + + Resources/pt_BR.lproj/Sparkle.strings + + hash + + /adUv04OXQkCFv+Oed6qktFVQ3E= + + hash2 + + lY5EZJwPc/Rmfhw1gotkeEKB+ANXqZUlM2G92sZwdJc= + + optional + + + Resources/pt_PT.lproj/Sparkle.strings + + hash + + Mji9loJOJvuDY9hz3FhQ4H+HY5E= + + hash2 + + RUq6VJjn/QyydkNbpklLwfCgRF62+uHhXen2dYLBNuQ= + + optional + + + Resources/ro.lproj/Sparkle.strings + + hash + + 9U+OTz29kXKZHY/nmvbtemMsB3g= + + hash2 + + NNvDsecglQ/utR6YEqxyMj5K976YRWieCIC/PZuWCtQ= + + optional + + + Resources/ru.lproj/Sparkle.strings + + hash + + VpSLGNvZ6sbRYsF23L8m6TG+P6E= + + hash2 + + wJZ5NG+mvj4anRFPUFyvSD0kGrg+ZAqklsPfHuCxLQY= + + optional + + + Resources/sk.lproj/Sparkle.strings + + hash + + qn/mo2EFOyw6keezS64Wo5ZGZXU= + + hash2 + + e3cyzJ87ohC1ff/BzZ5O00MnwRE02U+J1KwXlSZeSSg= + + optional + + + Resources/sl.lproj/Sparkle.strings + + hash + + kwvdisufBenuQzrVg8tYKTX+qgg= + + hash2 + + t8QC+9TBONwKLQvV3fKV0umsnAS8ZDpqPikVksFPtWc= + + optional + + + Resources/sv.lproj/Sparkle.strings + + hash + + 98/sk+A2Ew1fmKpuKZ3rq8eS1EM= + + hash2 + + mJY6aeXFnSx38bF630z5lNPmPtsoYVAwadh0KC+9vfQ= + + optional + + + Resources/th.lproj/Sparkle.strings + + hash + + HQwGW1Ebf0i+Bl4synks3x2SY2M= + + hash2 + + nlP7repbMz6EqHo3sZWnK3tzx47WKSWnULdUHCYPgKk= + + optional + + + Resources/tr.lproj/Sparkle.strings + + hash + + whUQco5F2wcYdjc+cPKlk+mtx7Q= + + hash2 + + xEXUfrylPld+eFGrPyj4wTRPj7vUWOZ2f94sWydq03M= + + optional + + + Resources/uk.lproj/Sparkle.strings + + hash + + JXhpqvLkX0yDWjbWgsk2wbSObKU= + + hash2 + + u0572QZYh6sB0GQdMGMePalOf4zkxE7YQG7pp898SEg= + + optional + + + Resources/zh_CN.lproj/Sparkle.strings + + hash + + OnR96Z9tB0noODRSYssSs63+zGA= + + hash2 + + zvMbFdgVGI0ls9vIRT+sie7dj2g1UjQu7iS+pOgyBo4= + + optional + + + Resources/zh_TW.lproj/Sparkle.strings + + hash + + 1FLKoM5jZ8JGBG/nmyEIA+/aalA= + + hash2 + + Vlf/4QD7/3S0SFqxmTWWcSwtTLWISKUSvLjpgWb7lxQ= + + optional + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Sparkle.framework/Versions/A/Resources/DarkAqua.css b/Sparkle.framework/Versions/A/Resources/DarkAqua.css new file mode 100644 index 00000000..a41e0f28 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/DarkAqua.css @@ -0,0 +1,9 @@ +html { + color: #FFFFFFD8; +} +:link { + color: #419CFF; +} +:link:active { + color: #FF1919; +} diff --git a/Sparkle.framework/Versions/A/Resources/Info.plist b/Sparkle.framework/Versions/A/Resources/Info.plist new file mode 100644 index 00000000..8786d487 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 20B28 + CFBundleDevelopmentRegion + en + CFBundleExecutable + Sparkle + CFBundleIdentifier + org.sparkle-project.Sparkle + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Sparkle + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.24.0 a-67-g0e162c98 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1.24.0 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12C5020f + DTPlatformName + macosx + DTPlatformVersion + 11.1 + DTSDKBuild + 20C5048g + DTSDKName + macosx11.1 + DTXcode + 1230 + DTXcodeBuild + 12C5020f + LSMinimumSystemVersion + 10.7 + + diff --git a/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist b/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist new file mode 100644 index 00000000..1f75b248 --- /dev/null +++ b/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist @@ -0,0 +1,314 @@ + + + + + ADP2,1 + Developer Transition Kit + iMac1,1 + iMac G3 (Rev A-D) + iMac4,1 + iMac (Core Duo) + iMac4,2 + iMac for Education (17 inch, Core Duo) + iMac5,1 + iMac (Core 2 Duo, 17 or 20 inch, SuperDrive) + iMac5,2 + iMac (Core 2 Duo, 17 inch, Combo Drive) + iMac6,1 + iMac (Core 2 Duo, 24 inch, SuperDrive) + iMac7,1 + iMac Intel Core 2 Duo (aluminum enclosure) + iMac8,1 + iMac (Core 2 Duo, 20 or 24 inch, Early 2008 ) + iMac9,1 + iMac (Core 2 Duo, 20 or 24 inch, Early or Mid 2009 ) + iMac10,1 + iMac (Core 2 Duo, 21.5 or 27 inch, Late 2009 ) + iMac11,1 + iMac (Core i5 or i7, 27 inch Late 2009) + iMac11,2 + 21.5" iMac (mid 2010) + iMac11,3 + iMac (Core i5 or i7, 27 inch Mid 2010) + iMac12,1 + iMac (Core i3 or i5 or i7, 21.5 inch Mid 2010 or Late 2011) + iMac12,2 + iMac (Core i5 or i7, 27 inch Mid 2011) + iMac13,1 + iMac (Core i3 or i5 or i7, 21.5 inch Late 2012 or Early 2013) + iMac13,2 + iMac (Core i5 or i7, 27 inch Late 2012) + iMac14,1 + iMac (Core i5, 21.5 inch Late 2013) + iMac14,2 + iMac (Core i5 or i7, 27 inch Late 2013) + iMac14,3 + iMac (Core i5 or i7, 21.5 inch Late 2013) + iMac14,4 + iMac (Core i5, 21.5 inch Mid 2014) + iMac15,1 + iMac (Retina 5K Core i5 or i7, 27 inch Late 2014 or Mid 2015) + iMac16,1 + iMac (Core i5, 21,5 inch Late 2015) + iMac16,2 + iMac (Retina 4K Core i5 or i7, 21.5 inch Late 2015) + iMac17,1 + iMac (Retina 5K Core i5 or i7, 27 inch Late 2015) + MacBook1,1 + MacBook (Core Duo) + MacBook2,1 + MacBook (Core 2 Duo) + MacBook4,1 + MacBook (Core 2 Duo Feb 2008) + MacBook5,1 + MacBook (Core 2 Duo, Late 2008, Unibody) + MacBook5,2 + MacBook (Core 2 Duo, Early 2009, White) + MacBook6,1 + MacBook (Core 2 Duo, Late 2009, Unibody) + MacBook7,1 + MacBook (Core 2 Duo, Mid 2010, White) + MacBook8,1 + MacBook (Core M, 12 inch, Early 2015) + MacBookAir1,1 + MacBook Air (Core 2 Duo, 13 inch, Early 2008) + MacBookAir2,1 + MacBook Air (Core 2 Duo, 13 inch, Mid 2009) + MacBookAir3,1 + MacBook Air (Core 2 Duo, 11 inch, Late 2010) + MacBookAir3,2 + MacBook Air (Core 2 Duo, 13 inch, Late 2010) + MacBookAir4,1 + MacBook Air (Core i5 or i7, 11 inch, Mid 2011) + MacBookAir4,2 + MacBook Air (Core i5 or i7, 13 inch, Mid 2011) + MacBookAir5,1 + MacBook Air (Core i5 or i7, 11 inch, Mid 2012) + MacBookAir5,2 + MacBook Air (Core i5 or i7, 13 inch, Mid 2012) + MacBookAir6,1 + MacBook Air (Core i5 or i7, 11 inch, Mid 2013 or Early 2014) + MacBookAir6,2 + MacBook Air (Core i5 or i7, 13 inch, Mid 2013 or Early 2014) + MacBookAir7,1 + MacBook Air (Core i5 or i7, 11 inch, Early 2015) + MacBookAir7,2 + MacBook Air (Core i5 or i7, 13 inch, Early 2015) + MacBookPro1,1 + MacBook Pro Core Duo (15-inch) + MacBookPro1,2 + MacBook Pro Core Duo (17-inch) + MacBookPro2,1 + MacBook Pro Core 2 Duo (17-inch) + MacBookPro2,2 + MacBook Pro Core 2 Duo (15-inch) + MacBookPro3,1 + MacBook Pro Core 2 Duo (15-inch LED, Core 2 Duo) + MacBookPro3,2 + MacBook Pro Core 2 Duo (17-inch HD, Core 2 Duo) + MacBookPro4,1 + MacBook Pro (Core 2 Duo Feb 2008) + MacBookPro5,1 + MacBook Pro Intel Core 2 Duo (aluminum unibody) + MacBookPro5,2 + MacBook Pro Intel Core 2 Duo (aluminum unibody) + MacBookPro5,3 + MacBook Pro Intel Core 2 Duo (aluminum unibody) + MacBookPro5,4 + MacBook Pro Intel Core 2 Duo (aluminum unibody) + MacBookPro5,5 + MacBook Pro Intel Core 2 Duo (aluminum unibody) + MacBookPro6,1 + MacBook Pro Intel Core i5, Intel Core i7 (mid 2010) + MacBookPro6,2 + MacBook Pro Intel Core i5, Intel Core i7 (mid 2010) + MacBookPro7,1 + MacBook Pro Intel Core 2 Duo (mid 2010) + MacBookPro8,1 + MacBook Pro Intel Core i5, Intel Core i7, 13" (early 2011) + MacBookPro8,2 + MacBook Pro Intel Core i7, 15" (early 2011) + MacBookPro8,3 + MacBook Pro Intel Core i7, 17" (early 2011) + MacBookPro9,1 + MacBook Pro (15-inch, Mid 2012) + MacBookPro9,2 + MacBook Pro (13-inch, Mid 2012) + MacBookPro10,1 + MacBook Pro (Retina, Mid 2012) + MacBookPro10,2 + MacBook Pro (Retina, 13-inch, Late 2012) + MacBookPro11,1 + MacBook Pro (Retina, 13-inch, Late 2013) + MacBookPro11,2 + MacBook Pro (Retina, 15-inch, Late 2013) + MacBookPro11,3 + MacBook Pro (Retina, 15-inch, Late 2013) + MacbookPro11,4 + MacBook Pro (Retina, 15-inch, Mid 2015) + MacbookPro11,5 + MacBook Pro (Retina, 15-inch, Mid 2015) + MacbookPro12,1  + MacBook Pro (Retina, 13-inch, Early 2015) + Macmini1,1 + Mac Mini (Core Solo/Duo) + Macmini2,1 + Mac mini Intel Core + Macmini3,1 + Mac mini Intel Core + Macmini4,1 + Mac mini Intel Core (Mid 2010) + Macmini5,1 + Mac mini (Core i5, Mid 2011) + Macmini5,2 + Mac mini (Core i5 or Core i7, Mid 2011) + Macmini5,3 + Mac mini (Core i7, Server, Mid 2011) + Macmini6,1 + Mac mini (Core i5, Late 2012) + Macmini6,2 + Mac mini (Core i7, Normal or Server, Late 2012) + Macmini7,1 + Mac mini (Core i5 or Core i7, Late 2014) + MacPro1,1,Quad + Mac Pro + MacPro1,1 + Mac Pro (four-core) + MacPro2,1 + Mac Pro (eight-core) + MacPro3,1 + Mac Pro (January 2008 4- or 8- core "Harpertown") + MacPro4,1 + Mac Pro (March 2009) + MacPro5,1 + Mac Pro (2010 or 2012) + MacPro6,1 + Mac Pro (Late 2013) + PowerBook1,1 + PowerBook G3 + PowerBook2,1 + iBook G3 + PowerBook2,2 + iBook G3 (FireWire) + PowerBook2,3 + iBook G3 + PowerBook2,4 + iBook G3 + PowerBook3,1 + PowerBook G3 (FireWire) + PowerBook3,2 + PowerBook G4 + PowerBook3,3 + PowerBook G4 (Gigabit Ethernet) + PowerBook3,4 + PowerBook G4 (DVI) + PowerBook3,5 + PowerBook G4 (1GHz / 867MHz) + PowerBook4,1 + iBook G3 (Dual USB, Late 2001) + PowerBook4,2 + iBook G3 (16MB VRAM) + PowerBook4,3 + iBook G3 Opaque 16MB VRAM, 32MB VRAM, Early 2003) + PowerBook5,1 + PowerBook G4 (17 inch) + PowerBook5,2 + PowerBook G4 (15 inch FW 800) + PowerBook5,3 + PowerBook G4 (17-inch 1.33GHz) + PowerBook5,4 + PowerBook G4 (15 inch 1.5/1.33GHz) + PowerBook5,5 + PowerBook G4 (17-inch 1.5GHz) + PowerBook5,6 + PowerBook G4 (15 inch 1.67GHz/1.5GHz) + PowerBook5,7 + PowerBook G4 (17-inch 1.67GHz) + PowerBook5,8 + PowerBook G4 (Double layer SD, 15 inch) + PowerBook5,9 + PowerBook G4 (Double layer SD, 17 inch) + PowerBook6,1 + PowerBook G4 (12 inch) + PowerBook6,2 + PowerBook G4 (12 inch, DVI) + PowerBook6,3 + iBook G4 + PowerBook6,4 + PowerBook G4 (12 inch 1.33GHz) + PowerBook6,5 + iBook G4 (Early-Late 2004) + PowerBook6,7 + iBook G4 (Mid 2005) + PowerBook6,8 + PowerBook G4 (12 inch 1.5GHz) + PowerMac1,1 + Power Macintosh G3 (Blue & White) + PowerMac1,2 + Power Macintosh G4 (PCI Graphics) + PowerMac2,1 + iMac G3 (Slot-loading CD-ROM) + PowerMac2,2 + iMac G3 (Summer 2000) + PowerMac3,1 + Power Macintosh G4 (AGP Graphics) + PowerMac3,2 + Power Macintosh G4 (AGP Graphics) + PowerMac3,3 + Power Macintosh G4 (Gigabit Ethernet) + PowerMac3,4 + Power Macintosh G4 (Digital Audio) + PowerMac3,5 + Power Macintosh G4 (Quick Silver) + PowerMac3,6 + Power Macintosh G4 (Mirrored Drive Door) + PowerMac4,1 + iMac G3 (Early/Summer 2001) + PowerMac4,2 + iMac G4 (Flat Panel) + PowerMac4,4 + eMac + PowerMac4,5 + iMac G4 (17-inch Flat Panel) + PowerMac5,1 + Power Macintosh G4 Cube + PowerMac5,2 + Power Mac G4 Cube + PowerMac6,1 + iMac G4 (USB 2.0) + PowerMac6,3 + iMac G4 (20-inch Flat Panel) + PowerMac6,4 + eMac (USB 2.0, 2005) + PowerMac7,2 + Power Macintosh G5 + PowerMac7,3 + Power Macintosh G5 + PowerMac8,1 + iMac G5 + PowerMac8,2 + iMac G5 (Ambient Light Sensor) + PowerMac9,1 + Power Macintosh G5 (Late 2005) + PowerMac10,1 + Mac Mini G4 + PowerMac10,2 + Mac Mini (Late 2005) + PowerMac11,2 + Power Macintosh G5 (Late 2005) + PowerMac12,1 + iMac G5 (iSight) + RackMac1,1 + Xserve G4 + RackMac1,2 + Xserve G4 (slot-loading, cluster node) + RackMac3,1 + Xserve G5 + Xserve1,1 + Xserve (Intel Xeon) + Xserve2,1 + Xserve (January 2008 quad-core) + Xserve3,1 + Xserve (early 2009) + + diff --git a/Sparkle.framework/Versions/A/Resources/SUStatus.nib b/Sparkle.framework/Versions/A/Resources/SUStatus.nib new file mode 100644 index 0000000000000000000000000000000000000000..f9c39a03214d3d57cf7158501fbbf896310e4a10 GIT binary patch literal 12667 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 z zqplzA`8fIGcR%U$$*vPgCw6|C^y$u%olfrhtn+8PPjxx9_w(+bA2{9X^x-cuzWCrw z&Y4fn=AHfgT=BUJ=ZBuZexdxr-HW!1KYUsD<)ce1IQBEID6cHM8hUm0wWw>`uXnn> z|3=1*6JHg6b?K)0=KZhTU;lY)@@@I;g?HNB*>pGl?!J3j_fCB?aR7wKKgCuzgqokHqK4s=ZBVlo`& zm=O2xFvlePCPhEN93c)<%ns%;#U?nB5>pZq7Vnq>pa}V?{6k6n`tdlBSTL@<9Z zf@%xl!9S_?R0rGz#tXxrQZ!nHR!}+)oNl`6z`@Y!-jV%D04z)`pdj8=27#&Jz?eWK z z!$MGoC(IWifVeE&?<+8Qsvza70|GUgb+cZ!o~=7g4F?X?QX@ES6;_#CmU3=fyV)XP z`70&VNXiTWS3a7ZVW}{AZ5}raL>K}0-_}5RxzAEI=9MWsLcqpr>8j;kZl>9FL6}kl ze`TgSf=S*9fsrU93oa7-Eswq`VNdJH?3tpj=T zkxC$L5<7%w6tTKsexJ#Cw$DUgpAB&E8VWHH(&u>g1=6R|*C!X{CcO=6Nyfc?$lWI)+7Ib1lGnocd-u2fSqun6ucB;dXf)>(&>z;x#VGII>> zx!guSg7mbY2C*-)I*6|uAc&j^zELUIa0y&5YX#R`v~Yi82-rP0JB7RdNxg<5*~zQ` zVld=n?IVL*6+DnywVFhg`Qg`a!vmBZ!V>^QB*K#f07fo6$KbgQ-(64$@QLyfAO!-X zK!6kokOBcxAV3NPP-J+x5=!kRy|#sw`wylCu=PJ(0Hyw;>HhOIQ6Ci-IVz|xsY}#l z>I!w0x&{a98`M|SP3mjv7ImAtL*1qBQQyE}`vLVW^&Ry+^^p33`jPsH`kDHL`jz^P zox#pzXR)){m)MutIqY0^9y_01z`nx1$}VIVv5VOy>{50ayPSQEeVtvwu4GrStJyW| zT6P_~o}HrfQ;OlZDOJjp{z|!0p$t$el`5rLsZj}}%Yh|dijj}Dr zJ6etSD*?bqx&^gtCE+Pt(4~XvRuHhwN#if#l34p{IvB-3B&roeIt;+~u5g34KU^@Z zM^oSy`3!s=-=;KF2;9|9g8SITphT0Y*QvdLF`S@IQm3fX)EVj=bpaT38JKe&7;~Tc zjryH>1UISM(2;Z;okC~Qd2}f~k~Y&8+D?z9C(+aCdGtnlGrbiMh&SmS^gHw}dN*JZ z`{)DoA^I$R74V3k1)+c~X$2hwVS)(36v4}a4TAlGtAdAqQojhl6u&&c9_IRO@_Wng zkl%5@9x83)4Z<

8^HAs1(ei^W657O_q26^|B=5swqk6E79760a9; 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 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdatePermissionPrompt.nib new file mode 100644 index 0000000000000000000000000000000000000000..4f215f0994566f82a1b9d47ee5b8252ae70b352e GIT binary patch literal 20579 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 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/ar.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ar.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..e00af341354a141ee0c9e9cc01189a91b5e2a79d GIT binary patch literal 8468 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 literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/cs.lproj/SUAutomaticUpdateAlert.nib new file mode 100644 index 0000000000000000000000000000000000000000..103886fe7b2696ac3728cfdb09f341fc1ca79e09 GIT binary patch literal 13594 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 I|20hbe~HT8r2qf` literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdatePermissionPrompt.nib new file mode 100644 index 0000000000000000000000000000000000000000..465e87dda1f938a9a703e2c27f5e570b5654dca9 GIT binary patch literal 20677 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 A$^ZZW literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/cs.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..02e077cf5d4b2ffb6b3d6be567187269610d2b9c GIT binary patch literal 10638 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 apBPRUF7oMEh$8-Rc(ESgyY;W(%KrnBryYO* literal 0 HcmV?d00001 diff --git a/Sparkle.framework/Versions/A/Resources/da.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/da.lproj/Sparkle.strings new file mode 100644 index 0000000000000000000000000000000000000000..e0957c6011630675e2ab1ac0f2d3f4930bfedc3c GIT binary patch literal 8306 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