From 32fb7872a765a04bb7c192d3cce083d6fb90cea7 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Sun, 31 Aug 2025 11:24:05 +0530 Subject: [PATCH 1/9] implemented network extension and chrome extension --- .../SelfControl-Development-Setup.md | 112 ++++ .../SelfControl.xcodeproj/project.pbxproj | 535 ++++++++++++++++++ .../xcschemes/SelfControl.xcscheme | 100 ++++ .../xcschemes/SelfControlExtension.xcscheme | 77 +++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++ .../SelfControl/Assets.xcassets/Contents.json | 6 + .../SelfControl/SelfControl/Info.plist | 38 ++ .../SelfControl/Main/ContentView.swift | 86 +++ .../SelfControl/Main/FilterViewModel.swift | 311 ++++++++++ .../SelfControl/Main/SCDurationSlider.swift | 88 +++ .../SelfControl/SelfControl/Main/Status.swift | 39 ++ .../Preferences/PreferencesView.swift | 69 +++ .../Preferences/ProxyPreferences.swift | 27 + .../Preview Assets.xcassets/Contents.json | 6 + .../SelfControl/SelfControl/README.md | 9 + .../SelfControl/SelfControl.entitlements | 22 + .../SelfControl/SelfControlApp.swift | 25 + .../DNSProxyProvider.swift | 108 ++++ .../SelfControlExtension/DNSQuestion.swift | 72 +++ .../ExtensionsPreferencesManager.swift | 24 + .../FilterDataProvider.swift | 279 +++++++++ .../SelfControlExtension/IPCConnection.swift | 198 +++++++ .../SelfControlExtension/Info.plist | 16 + .../SelfControlExtension/PlistListner.swift | 58 ++ .../SelfControlExtension/README.md | 8 + .../SelfControlExtension.entitlements | 18 + .../SelfControlExtension/String+URL.swift | 35 ++ .../SelfControlExtension/main.swift | 18 + 29 files changed, 2453 insertions(+) create mode 100644 Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md create mode 100644 Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj create mode 100644 Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme create mode 100644 Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Info.plist create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Self-Control-Extension/SelfControl/SelfControl/README.md create mode 100644 Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements create mode 100644 Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/README.md create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/main.swift diff --git a/Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md b/Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md new file mode 100644 index 0000000..4d3e988 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl-Development-Setup.md @@ -0,0 +1,112 @@ +# SelfControl Development Setup Guide + +This document outlines the exact setup needed for developing the SelfControl app that uses NetworkExtension for filtering network traffic. + +## Prerequisites + +- Xcode 16.0 or later +- macOS (15.0) or later +- Apple Developer account with Network Extension entitlements +- Administrator privileges on your Mac + +## Development Setup Steps + +### 1. Disable System Integrity Protection (SIP) + +For NetworkExtension development, SIP needs to be disabled to allow proper installation and uninstallation of system extensions during development: + +1. Boot into Recovery Mode (restart holding Cmd+R) +2. Open Terminal from Utilities menu +3. Run: `csrutil disable` +4. Restart your Mac + +> ⚠️ **IMPORTANT**: Disabling SIP reduces your system's security. Only do this on a development machine, not on your primary production machine. + +### 2. Configure Post-Build Actions + +To ensure fresh installs of the system extension during development, add two critical post-build actions to your Xcode project: + +1. Open your project in Xcode +2. Select your main app target +3. Go to "Build Phases" +4. Add a new "Run Script" phase (click the + button) +5. Add the following script: + +```bash +# Uninstall the previous system extension first +systemextensionsctl uninstall A4L93BSQEG com.application.SelfControl.SelfControlExtension + +# Copy the app to /Applications +ditto "${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}" "/Applications/${FULL_PRODUCT_NAME}" +``` + +> Note: Replace `A4L93BSQEG` with your actual Team ID and `com.application.SelfControl.SelfControlExtension` with your actual extension bundle ID if different. + +### 3. Bundle Identifier Setup + +Ensure your bundle identifiers are set up correctly: + +- Main App: `com.application.SelfControl` +- Extension: `com.application.SelfControl.SelfControlExtension` + +### 4. Configure Info.plist for Network Extension + +In your Network Extension's Info.plist, ensure proper configuration: + +```xml +NetworkExtension + + NEMachServiceName + $(TeamIdentifierPrefix)com.application.SelfControl.SelfControlExtension + NEProviderClasses + + com.apple.networkextension.filter-data + $(PRODUCT_MODULE_NAME).FilterDataProvider + + +``` + +### 5. Development Workflow + +With this setup, your development workflow becomes: + +1. Make code changes in Xcode +2. Build the project (⌘B) + - This will automatically uninstall the previous extension + - Then copy the new build to /Applications +3. Launch the app from /Applications (not from Xcode) +4. Check Console.app for logs with the prefix `[EADBUG]` + +### 6. Troubleshooting + +If you encounter issues: + +- Verify the app is properly copied to `/Applications` +- Check that the system extension uninstall command is working correctly +- Inspect Console.app for any error messages +- Verify your Team ID is correct in the post-build script +- Ensure bundle identifiers match between the app, extension, and post-build script + +### 7. Re-enabling SIP After Development + +When you're done with development and ready to deploy: + +1. Boot into Recovery Mode +2. Run: `csrutil enable` +3. Restart your Mac + +## Known Limitations + +- Each build requires a fresh installation due to NetworkExtension constraints +- The app must run from /Applications to function properly +- With SIP disabled, your development machine has reduced security protections +- NetworkExtension development generally requires more manual steps than typical app development + +## Conclusion + +This setup automates the most tedious parts of NetworkExtension development by: +1. Ensuring old extensions are uninstalled before new ones are installed +2. Automatically placing the app in the required location (/Applications) +3. Allowing for rapid iteration despite NetworkExtension's constraints + +By following these specific steps, you should be able to develop your NetworkExtension-based app more efficiently. \ No newline at end of file diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9f18791 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj @@ -0,0 +1,535 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 77BA53092D9D5E2A00863476 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */; }; + 9A441CE32E36024E00A521CC /* com.application.SelfControl.corebits.network.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 9A441CE12E36024300A521CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 77BA52CE2D9D5D7700863476 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 77BA53052D9D5E2A00863476; + remoteInfo = SelfControlExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9AFC129E2E355671003B5455 /* Embed System Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(SYSTEM_EXTENSIONS_FOLDER_PATH)"; + dstSubfolderSpec = 16; + files = ( + 9A441CE32E36024E00A521CC /* com.application.SelfControl.corebits.network.systemextension in Embed System Extensions */, + ); + name = "Embed System Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 77BA52D62D9D5D7700863476 /* SelfControl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SelfControl.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = com.application.SelfControl.corebits.network.systemextension; sourceTree = BUILT_PRODUCTS_DIR; }; + 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 77BA53142D9D5E2A00863476 /* Exceptions for "SelfControlExtension" folder in "SelfControlExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + DNSProxyProvider.swift, + DNSQuestion.swift, + Info.plist, + ); + target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */; + }; + 9A6873742E280E320054AD3F /* Exceptions for "SelfControl" folder in "SelfControlExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Preferences/ProxyPreferences.swift, + ); + target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */; + }; + 9A6873762E2814B30054AD3F /* Exceptions for "SelfControlExtension" folder in "SelfControl" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + IPCConnection.swift, + PlistListner.swift, + "String+URL.swift", + ); + target = 77BA52D52D9D5D7700863476 /* SelfControl */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 77BA52D82D9D5D7700863476 /* SelfControl */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 9A6873742E280E320054AD3F /* Exceptions for "SelfControl" folder in "SelfControlExtension" target */, + ); + path = SelfControl; + sourceTree = ""; + }; + 77BA530A2D9D5E2A00863476 /* SelfControlExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 9A6873762E2814B30054AD3F /* Exceptions for "SelfControlExtension" folder in "SelfControl" target */, + 77BA53142D9D5E2A00863476 /* Exceptions for "SelfControlExtension" folder in "SelfControlExtension" target */, + ); + path = SelfControlExtension; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 77BA52D32D9D5D7700863476 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 77BA53032D9D5E2A00863476 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 77BA53092D9D5E2A00863476 /* NetworkExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 77BA52CD2D9D5D7700863476 = { + isa = PBXGroup; + children = ( + 77BA52D82D9D5D7700863476 /* SelfControl */, + 77BA530A2D9D5E2A00863476 /* SelfControlExtension */, + 77BA53072D9D5E2A00863476 /* Frameworks */, + 77BA52D72D9D5D7700863476 /* Products */, + ); + sourceTree = ""; + }; + 77BA52D72D9D5D7700863476 /* Products */ = { + isa = PBXGroup; + children = ( + 77BA52D62D9D5D7700863476 /* SelfControl.app */, + 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */, + ); + name = Products; + sourceTree = ""; + }; + 77BA53072D9D5E2A00863476 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 77BA52D52D9D5D7700863476 /* SelfControl */ = { + isa = PBXNativeTarget; + buildConfigurationList = 77BA52E52D9D5D7A00863476 /* Build configuration list for PBXNativeTarget "SelfControl" */; + buildPhases = ( + 77BA52D22D9D5D7700863476 /* Sources */, + 77BA52D32D9D5D7700863476 /* Frameworks */, + 77BA52D42D9D5D7700863476 /* Resources */, + 9AFC129E2E355671003B5455 /* Embed System Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 9A441CE22E36024300A521CC /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 77BA52D82D9D5D7700863476 /* SelfControl */, + ); + name = SelfControl; + packageProductDependencies = ( + ); + productName = SelfControl; + productReference = 77BA52D62D9D5D7700863476 /* SelfControl.app */; + productType = "com.apple.product-type.application"; + }; + 77BA53052D9D5E2A00863476 /* SelfControlExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 77BA53152D9D5E2A00863476 /* Build configuration list for PBXNativeTarget "SelfControlExtension" */; + buildPhases = ( + 77BA53022D9D5E2A00863476 /* Sources */, + 77BA53032D9D5E2A00863476 /* Frameworks */, + 77BA53042D9D5E2A00863476 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 77BA530A2D9D5E2A00863476 /* SelfControlExtension */, + ); + name = SelfControlExtension; + packageProductDependencies = ( + ); + productName = SelfControlExtension; + productReference = 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */; + productType = "com.apple.product-type.system-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 77BA52CE2D9D5D7700863476 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 77BA52D52D9D5D7700863476 = { + CreatedOnToolsVersion = 16.2; + }; + 77BA53052D9D5E2A00863476 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = 77BA52D12D9D5D7700863476 /* Build configuration list for PBXProject "SelfControl" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 77BA52CD2D9D5D7700863476; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 77BA52D72D9D5D7700863476 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 77BA52D52D9D5D7700863476 /* SelfControl */, + 77BA53052D9D5E2A00863476 /* SelfControlExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 77BA52D42D9D5D7700863476 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 77BA53042D9D5E2A00863476 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 77BA52D22D9D5D7700863476 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 77BA53022D9D5E2A00863476 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 9A441CE22E36024300A521CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */; + targetProxy = 9A441CE12E36024300A521CC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 77BA52E32D9D5D7A00863476 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 77BA52E42D9D5D7A00863476 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 77BA52E62D9D5D7A00863476 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SelfControl/SelfControl.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\""; + DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = SelfControl/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 77BA52E72D9D5D7A00863476 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = SelfControl/SelfControl.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\""; + DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = SelfControl/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 77BA53162D9D5E2A00863476 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = SelfControlExtension/SelfControlExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SelfControlExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = SelfControlExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "DNS lookup"; + INFOPLIST_KEY_NSSystemExtensionUsageDescription = "testing extension"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network; + PRODUCT_NAME = "$(inherited)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 77BA53172D9D5E2A00863476 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = SelfControlExtension/SelfControlExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = SelfControlExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = SelfControlExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_NSLocalNetworkUsageDescription = "DNS lookup"; + INFOPLIST_KEY_NSSystemExtensionUsageDescription = "testing extension"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network; + PRODUCT_NAME = "$(inherited)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 77BA52D12D9D5D7700863476 /* Build configuration list for PBXProject "SelfControl" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 77BA52E32D9D5D7A00863476 /* Debug */, + 77BA52E42D9D5D7A00863476 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 77BA52E52D9D5D7A00863476 /* Build configuration list for PBXNativeTarget "SelfControl" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 77BA52E62D9D5D7A00863476 /* Debug */, + 77BA52E72D9D5D7A00863476 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 77BA53152D9D5E2A00863476 /* Build configuration list for PBXNativeTarget "SelfControlExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 77BA53162D9D5E2A00863476 /* Debug */, + 77BA53172D9D5E2A00863476 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 77BA52CE2D9D5D7700863476 /* Project object */; +} diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme new file mode 100644 index 0000000..3780773 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControl.xcscheme @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme new file mode 100644 index 0000000..97fcc93 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/xcshareddata/xcschemes/SelfControlExtension.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Info.plist b/Self-Control-Extension/SelfControl/SelfControl/Info.plist new file mode 100644 index 0000000..d876c18 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2025 SelfControl. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + NSLocalNetworkUsageDescription + DNS lookup + + diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift new file mode 100644 index 0000000..7dfa696 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift @@ -0,0 +1,86 @@ +// +// ContentView.swift +// SelfControl +// +// Created by Egzon Arifi on 02/04/2025. +// + +import SwiftUI +import NetworkExtension +import SystemExtensions +import os.log +import Cocoa + +struct ContentView: View { + @EnvironmentObject var viewModel: FilterViewModel + @Environment(\.openWindow) private var openWindow + + var body: some View { + VStack(spacing: 20) { + // Status indicator with image and text. + Button { + viewModel.activateExtension() + } label: { + Text("Install and Start Block") + } + HStack { + Spacer() + Slider(value: $viewModel.delay, in: 1...60) { + Text("Time: \(viewModel.delay, specifier: "%.1f") Minutes") + } + Spacer() + } + statusView + // Show a progress indicator when in the indeterminate state. + if viewModel.status == .indeterminate { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + + // Start/Stop buttons. + HStack { + if viewModel.status == .stopped { + Button("Start") { + viewModel.startFilter() + + } + } + if viewModel.status == .running { + Button("Stop") { + viewModel.stopFilter() + } + } + } +// Button { +// viewModel.setBlockedUrls(urls: ProxyPreferences.getBlockedDomains()) +// } label: { +// Text("Enable Url Blocking") +// } + } + .padding() + .frame(minWidth: 150, minHeight: 150) + .onDisappear { +// let urls = ProxyPreferences.getBlockedDomains() + + } + } +} + +private extension ContentView { + var statusView: some View { + HStack { + viewModel.status.color + .clipShape(Circle()) + .frame(width: 20, height: 20) + Text("Status: \(viewModel.status.text)") + Button("Edit Blocklist") { + openWindow(id: "preferences") + + } + } + } +} + +#Preview { + ContentView() +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift new file mode 100644 index 0000000..8f05055 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift @@ -0,0 +1,311 @@ +// +// FilterViewModel.swift +// SelfControl +// +// Created by Egzon Arifi on 02/04/2025. +// + +import SwiftUI +import NetworkExtension +import SystemExtensions +import os.log +import Cocoa + +final class FilterViewModel: NSObject, ObservableObject, OSSystemExtensionRequestDelegate, AppCommunication { + @Published var status: Status = .stopped + @State private var domains = ProxyPreferences.getBlockedDomains() + private let listner = PlistListner() + @Published var delay: Double = 0.0 + + // Date formatter used to log entries + lazy var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return formatter + }() + + // Observer for filter configuration changes + var observer: Any? + var extensionIdentifier: String? + + // Load the system extension bundle from the app’s Contents/Library/SystemExtensions folder. + lazy var extensionBundle: Bundle = { + let extensionsDirectoryURL = URL(fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL) + let extensionURLs: [URL] + do { + extensionURLs = try FileManager.default.contentsOfDirectory(at: extensionsDirectoryURL, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles) + } catch let error { + fatalError("Failed to get the contents of \(extensionsDirectoryURL.absoluteString): \(error.localizedDescription)") + } + guard let extensionURL = extensionURLs.first else { + fatalError("Failed to find any system extensions") + } + guard let extensionBundle = Bundle(url: extensionURL) else { + fatalError("Failed to create a bundle with URL \(extensionURL.absoluteString)") + } + return extensionBundle + }() + + override init() { + super.init() + onInit() + self.extensionIdentifier = extensionBundle.bundleIdentifier + self.listner.blockeddomainFetcher = { + return ProxyPreferences.getBlockedDomains() + } + self.listner.startListening() + } + + deinit { + if let observer = observer { + NotificationCenter.default.removeObserver(observer, name: .NEFilterConfigurationDidChange, object: NEFilterManager.shared()) + } + } + + func onInit() { + // On initialization load the filter configuration and register for changes. + loadFilterConfiguration { success in + guard success else { + self.status = .stopped + return + } + self.updateStatus() + self.observer = NotificationCenter.default.addObserver(forName: .NEFilterConfigurationDidChange, + object: NEFilterManager.shared(), + queue: .main) { [weak self] _ in + self?.updateStatus() + } + } + } + + // MARK: - UI and Filter Management + + func setBlockedUrls(urls: [String]) { + IPCConnection.shared.enableURLBlocking(urls) + } + + func updateStatus() { + if NEFilterManager.shared().isEnabled { + registerWithProvider() + } else { + status = .stopped + } + } + + func logFlow(_ flowInfo: [String: String], at date: Date, userAllowed: Bool) { + guard let localPort = flowInfo[FlowInfoKey.localPort.rawValue], + let remoteAddress = flowInfo[FlowInfoKey.remoteAddress.rawValue] else { + return + } + let dateString = dateFormatter.string(from: date) + let message = "\(dateString) \(userAllowed ? "ALLOW" : "DENY") \(localPort) <-- \(remoteAddress)\n" + os_log("[SC] 🔍] %@", message) + } + + func loadFilterConfiguration(completionHandler: @escaping (Bool) -> Void) { + NEFilterManager.shared().loadFromPreferences { loadError in + DispatchQueue.main.async { + var success = true + if let error = loadError { + os_log("[SC] 🔍] Failed to load the filter configuration: %@", error.localizedDescription) + success = false + } + completionHandler(success) + } + } + } + + func enableFilterConfiguration() { + let filterManager = NEFilterManager.shared() + guard !filterManager.isEnabled else { + registerWithProvider() + return + } + loadFilterConfiguration { success in + guard success else { + self.status = .stopped + return + } + if filterManager.providerConfiguration == nil { + let providerConfiguration = NEFilterProviderConfiguration() + providerConfiguration.filterSockets = true + providerConfiguration.filterPackets = false + filterManager.providerConfiguration = providerConfiguration + if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String { + filterManager.localizedDescription = appName + } + } + filterManager.isEnabled = true + filterManager.saveToPreferences { saveError in + DispatchQueue.main.async { + if let error = saveError { + os_log("[SC] 🔍] Failed to save the filter configuration: %@", error.localizedDescription) + self.status = .stopped + return + } else { +// self.enableDNSProxy() + } + self.registerWithProvider() + } + } + } + } + + func enableDNSProxy() { + let manager = NEDNSProxyManager.shared() + + manager.loadFromPreferences { error in + guard error == nil else { return } + + let proto = NEDNSProxyProviderProtocol() + proto.providerBundleIdentifier = "com.application.SelfControl.corebits.network" + proto.serverAddress = "127.0.0.1" // placeholder +// proto.filterSockets = true + manager.localizedDescription = "DNS Logger" + manager.providerProtocol = proto + manager.isEnabled = true + + manager.saveToPreferences { saveError in + if let saveError = saveError { + print("Failed to save: \(saveError)") + } else { + print("DNS proxy saved.") + } + } + } + } + + func registerWithProvider() { + // Assuming an IPCConnection singleton similar to the AppKit sample + IPCConnection.shared.register(withExtension: extensionBundle, delegate: self) { success in + DispatchQueue.main.async { + self.status = success ? .running : .stopped + self.setBlockedUrls(urls: ProxyPreferences.getBlockedDomains()) + } +// setBlockedURLs([]) + } + } + + func activateExtension() { + // Start by activating the system extension. + guard let extensionIdentifier = extensionIdentifier else { + self.status = .stopped + return + } +// let request = OSSystemExtensionRequest.propertiesRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main) +// request.delegate = self +// OSSystemExtensionManager.shared.submitRequest(request) + let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main) + activationRequest.delegate = self + OSSystemExtensionManager.shared.submitRequest(activationRequest) + } + // MARK: - UI Event Handlers. + + func startFilter() { + status = .indeterminate + guard !NEFilterManager.shared().isEnabled else { + registerWithProvider() + return + } +// guard let extensionIdentifier = extensionBundle.bundleIdentifier else { +// status = .stopped +// return +// } + activateExtension() + } + + func stopFilter() { + let filterManager = NEFilterManager.shared() + status = .indeterminate + guard filterManager.isEnabled else { + status = .stopped + return + } + loadFilterConfiguration { success in + guard success else { + self.status = .running + return + } + // Disable the content filter configuration. + filterManager.isEnabled = false + filterManager.saveToPreferences { saveError in + DispatchQueue.main.async { + if let error = saveError { + os_log("[SC] 🔍] Failed to disable the filter configuration: %@", error.localizedDescription) + self.status = .running + return + } + self.status = .stopped + } + } + } + } + // MARK: - OSSystemExtensionRequestDelegate Methods + + func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) { + guard result == .completed else { + os_log("[SC] 🔍] Unexpected result %d for system extension request", result.rawValue) + status = .stopped + return + } + enableFilterConfiguration() + } + + func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) { + os_log("[SC] 🔍] System extension request failed: %@", error.localizedDescription) + status = .stopped + } + + func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { + os_log("[SC] 🔍] Extension %@ requires user approval", request.identifier) + } + + func request(_ request: OSSystemExtensionRequest, + actionForReplacingExtension existing: OSSystemExtensionProperties, + withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction { + os_log("[SC] 🔍] Replacing extension %@ version %@ with version %@", request.identifier, existing.bundleShortVersion, ext.bundleShortVersion) + return .replace + } + + func request(_ request: OSSystemExtensionRequest, foundProperties properties: [OSSystemExtensionProperties]) { + os_log("[SC] 🔍] foundProperties extension %@", properties) + } + + // MARK: - App Communication (Prompting the User) + + @objc func promptUser(aboutFlow flowInfo: [String: String], responseHandler: @escaping (Bool) -> Void) { + guard let localPort = flowInfo[FlowInfoKey.localPort.rawValue], + let remoteAddress = flowInfo[FlowInfoKey.remoteAddress.rawValue] else { + os_log("[SC] 🔍] Got a promptUser call without valid flow info: %@", flowInfo) + responseHandler(true) + return + } + let connectionDate = Date() + DispatchQueue.main.async { + // For SwiftUI on macOS, use NSAlert via the shared NSApplication window. + if let window = NSApplication.shared.windows.first { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = "New incoming connection" + alert.informativeText = "A new connection on port \(localPort) has been received from \(remoteAddress)." + alert.addButton(withTitle: "Allow") + alert.addButton(withTitle: "Deny") + alert.beginSheetModal(for: window) { response in + let userAllowed = (response == .alertFirstButtonReturn) + self.logFlow(flowInfo, at: connectionDate, userAllowed: userAllowed) + responseHandler(userAllowed) + } + } else { + // Fallback if no window is available. + self.logFlow(flowInfo, at: connectionDate, userAllowed: true) + responseHandler(true) + } + } + } + + func didSetUrls() { + print("didSetUrls+++++") + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift new file mode 100644 index 0000000..4b2487e --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/SCDurationSlider.swift @@ -0,0 +1,88 @@ +// +// SCDurationSlider.swift +// SelfControl +// +// Created by Satendra Singh on 31/08/25. +// + +import Foundation + + +//import Foundation +//import AppKit + +//class SCTimeIntervalFormatter { +// +// func string(for obj: Any?) -> String { +// guard let number = obj as? NSNumber else { +// return "" +// } +// return formatSeconds(number.doubleValue) +// } +// +// func formatSeconds(_ seconds: TimeInterval) -> String { +// let useModernBehavior = NSAppKitVersion.current >= NSAppKitVersion.macOS10_8 +// if useModernBehavior { +// return formatSecondsUsingModernBehavior(seconds) +// } else { +// return formatSecondsUsingLegacyBehavior(seconds) +// } +// } +// +// private func formatSecondsUsingModernBehavior(_ seconds: TimeInterval) -> String { +// struct FormatterHolder { +// static let formatter: TTTTimeIntervalFormatter = { +// let formatter = NSDateFormatter() +// formatter.pastDeicticExpression = "" +// formatter.presentDeicticExpression = "" +// formatter.futureDeicticExpression = "" +// formatter.significantUnits = [.year, .month, .day, .hour, .minute] +// formatter.numberOfSignificantUnits = 0 +// formatter.leastSignificantUnit = .minute +// return formatter +// }() +// } +// +// var formatted = FormatterHolder.formatter.string(forTimeInterval: seconds) ?? "" +// if formatted.isEmpty { +// formatted = stringIndicatingZeroMinutes() +// } +// +// return formatted +// } +// +// private func formatSecondsUsingLegacyBehavior(_ seconds: TimeInterval) -> String { +// let numMinutes = Int(seconds / 60) +// let formatDays = numMinutes / 1440 +// let formatHours = (numMinutes % 1440) / 60 +// let formatMinutes = numMinutes % 60 +// +// var timeString = "" +// +// if numMinutes > 0 { +// if formatDays > 0 { +// timeString = "\(formatDays) " + (formatDays == 1 ? NSLocalizedString("day", comment: "Single day time string") : NSLocalizedString("days", comment: "Plural days time string")) +// } +// if formatHours > 0 { +// let hoursPart = "\(formatHours) " + (formatHours == 1 ? NSLocalizedString("hour", comment: "Single hour time string") : NSLocalizedString("hours", comment: "Plural hours time string")) +// timeString += (formatDays > 0 ? ", " : "") + hoursPart +// } +// if formatMinutes > 0 { +// let minutesPart = "\(formatMinutes) " + (formatMinutes == 1 ? NSLocalizedString("minute", comment: "Single minute time string") : NSLocalizedString("minutes", comment: "Plural minutes time string")) +// timeString += ((formatHours > 0 || formatDays > 0) ? ", " : "") + minutesPart +// } +// } else { +// timeString = stringIndicatingZeroMinutes() +// } +// +// return timeString +// } +// +// private func stringIndicatingZeroMinutes() -> String { +// return String( +// format: "0 %@ (%@)", +// NSLocalizedString("minutes", comment: "Plural minutes time string"), +// NSLocalizedString("disabled", comment: "Shows that SelfControl is disabled") +// ) +// } +//} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift new file mode 100644 index 0000000..cb8c962 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/Status.swift @@ -0,0 +1,39 @@ +// +// Status.swift +// SelfControl +// +// Created by Egzon Arifi on 02/04/2025. +// + +import Foundation +import SwiftUI + +enum Status { + case stopped + case indeterminate + case running +} + +extension Status { + var text: String { + switch self { + case .stopped: + return "Stopped" + case .indeterminate: + return "Indeterminate" + case .running: + return "Running" + } + } + + var color: Color { + switch self { + case .stopped: + return .red + case .indeterminate: + return .yellow + case .running: + return .green + } + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift b/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift new file mode 100644 index 0000000..6cacbf4 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift @@ -0,0 +1,69 @@ +// +// PreferencesView.swift +// SelfControl +// +// Created by Satendra Singh on 12/07/25. +// + +import SwiftUI + + +struct PreferencesView: View { + @State private var domains = ProxyPreferences.getBlockedDomains() + @State private var newDomain = "" + @EnvironmentObject var viewModel: FilterViewModel + + var body: some View { + VStack { + List { + ForEach(domains, id: \.self) { domain in + HStack { + Text(domain) + Spacer() + Button(action: { + deleteItem(domain: domain) + }) { + Image(systemName: "trash") + .foregroundColor(.red) + } + .buttonStyle(BorderlessButtonStyle()) // ensures button works inside List + } + } + .onDelete(perform: delete) // still supports swipe-to-delete + } + + HStack { + TextField("Add Domain", text: $newDomain) + Button("Add") { + addDomain() + } + } + + Button("Save Preferences") { + ProxyPreferences.setBlockedDomains(domains) + viewModel.setBlockedUrls(urls: domains) + } + } + .padding() + .frame(width: 400, height: 300) + } + + func addDomain() { + guard !newDomain.isEmpty else { return } + guard let domainValue = newDomain.domainString else { return } + domains.append(domainValue) + newDomain = "" + ProxyPreferences.setBlockedDomains(domains) + } + + func delete(at offsets: IndexSet) { + domains.remove(atOffsets: offsets) + ProxyPreferences.setBlockedDomains(domains) + } + + func deleteItem(domain: String) { + if let index = domains.firstIndex(of: domain) { + domains.remove(at: index) + } + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift b/Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift new file mode 100644 index 0000000..4ba2f0b --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Preferences/ProxyPreferences.swift @@ -0,0 +1,27 @@ +// +// ProxyPreferences.swift +// SelfControl +// +// Created by Satendra Singh on 12/07/25. +// + + +import Foundation + +struct ProxyPreferences { +// static let appGroup = "X6FQ433AWK.com.application.SelfControl.Extension" + static let blockedDomainsKey = "BlockedDomains" + + static func getBlockedDomains() -> [String] { + let defaults = UserDefaults.standard +// let defaults = UserDefaults(suiteName: appGroup) + + return defaults.stringArray(forKey: blockedDomainsKey) ?? [] + } + + static func setBlockedDomains(_ domains: [String]) { +// let defaults = UserDefaults(suiteName: appGroup) + let defaults = UserDefaults.standard + defaults.set(domains, forKey: blockedDomainsKey) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json b/Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/README.md b/Self-Control-Extension/SelfControl/SelfControl/README.md new file mode 100644 index 0000000..dae091d --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/README.md @@ -0,0 +1,9 @@ +# SelfControlApp + +This directory contains the Swift-based macOS app for SelfControl. + +## Structure + +- `UI/` - User interface code +- `ViewControllers/` - View controllers +- `Resources/` - App-specific resources \ No newline at end of file diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements new file mode 100644 index 0000000..c00a50a --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements @@ -0,0 +1,22 @@ + + + + + com.apple.developer.networking.networkextension + + content-filter-provider + + com.apple.developer.system-extension.install + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.application.SelfControl.corebits + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.server + + + diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift b/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift new file mode 100644 index 0000000..2dfb682 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift @@ -0,0 +1,25 @@ +// +// SelfControlApp.swift +// SelfControl +// +// Created by Egzon Arifi on 02/04/2025. +// + +import SwiftUI + +@main +struct SelfControlApp: App { + @StateObject var viewModel = FilterViewModel() + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(viewModel) // Inject the object into the environment + + } + Window("Preferences View", id: "preferences") { + PreferencesView() // Your view to be presented in the new window + .environmentObject(viewModel) // Inject the object into the environment + } + .windowStyle(.automatic) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift new file mode 100644 index 0000000..15a0ab3 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSProxyProvider.swift @@ -0,0 +1,108 @@ +// +// DNSProxyProvider.swift +// SelfControl +// +// Created by Satendra Singh on 02/08/25. +// + + +import NetworkExtension +import os.log + +class DNSProxyProvider: NEDNSProxyProvider { + + // Cache for queried domains (thread-safe) + var observedDomains = Set() + let queue = DispatchQueue(label: "dns.sync.queue") + + override func startProxy(options: [String : Any]? = nil, completionHandler: @escaping (Error?) -> Void) { + NSLog("DNS Proxy started") + os_log("SC] 🔍 DNS Proxy started") + completionHandler(nil) + } + + override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + if let tcpFlow = flow as? NEAppProxyTCPFlow { + os_log("SC] 🔍 DNS Proxy NEAppProxyTCPFlow") + readFromTCP(flow: tcpFlow) + return true + } + + guard let udpFlow = flow as? NEAppProxyUDPFlow else { + return true + } + os_log("SC] 🔍 DNS Proxy NEAppProxyUDPFlow") + udpFlow.readDatagrams { datagrams, remoteEndpoints, error in + guard let datagrams = datagrams else { return } + + for data in datagrams { + if let message = DNSParser.parseMessage(data) { + for question in message.questions { + os_log("SC] 🔍 DNS Query for domain: %{public}@", question.name) + NSLog("🔍 DNS Query for domain: \(question.name)") + // Save domain name, blocklist, etc. + } + } + } + + // Respond or forward if you're proxying +// udpFlow.closeReadWithError(nil) +// udpFlow.closeWriteWithError(nil) + } + + return true + } + + override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + NSLog("DNS Proxy stopped") + os_log("SC] 🔍 DNS Proxy stopped") + completionHandler() + } + + func readFromTCP(flow: NEAppProxyTCPFlow) { + flow.readData { [weak self] data, error in + guard let data = data, !data.isEmpty else { + os_log("SC] 🔍 No data or error: : %{public}@", error?.localizedDescription ?? "Empty") + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + return + } + + var offset = 0 58540 + + while offset + 2 <= data.count { + // DNS over TCP starts with 2-byte length prefix + let length = Int(data.uint16(at: offset)) + offset += 2 + + guard offset + length <= data.count else { + NSLog("Incomplete DNS message") + os_log("SC] 🔍 Incomplete DNS message") + break + } + + let dnsData = data.subdata(in: offset..<(offset + length)) + if let message = DNSParser.parseMessage(dnsData) { + for q in message.questions { + NSLog("🌐 DNS (TCP) Query: \(q.name)") + os_log("SC] 🔍 🌐 (TCP) Query: %{public}@", q.name) + } + } + + offset += length + } + + // Optionally respond or forward, or keep reading + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + } + } + +} + +extension Data { + public func uint16(at offset: Int) -> UInt16 { + guard offset + 1 < self.count else { return 0 } + return UInt16(self[offset]) << 8 | UInt16(self[offset + 1]) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift new file mode 100644 index 0000000..8f00f1d --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/DNSQuestion.swift @@ -0,0 +1,72 @@ +// +// DNSQuestion.swift +// SelfControl +// +// Created by Satendra Singh on 02/08/25. +// + +import Foundation + +struct DNSQuestion { + let name: String + let type: UInt16 + let qclass: UInt16 +} + +struct DNSMessage { + let id: UInt16 + let isQuery: Bool + let opcode: UInt8 + let questions: [DNSQuestion] +} + +class DNSParser { + static func parseMessage(_ data: Data) -> DNSMessage? { + guard data.count >= 12 else { return nil } // DNS header size + + let id = data.uint16(at: 0) + let flags = data.uint16(at: 2) + + let isQuery = (flags & 0x8000) == 0 + let opcode = UInt8((flags & 0x7800) >> 11) + let qdcount = data.uint16(at: 4) + + var offset = 12 + + var questions: [DNSQuestion] = [] + + for _ in 0..= offset + 4 else { return nil } + let type = data.uint16(at: offset) + let qclass = data.uint16(at: offset + 2) + offset += 4 + questions.append(DNSQuestion(name: name, type: type, qclass: qclass)) + } + + return DNSMessage(id: id, isQuery: isQuery, opcode: opcode, questions: questions) + } + + private static func parseDomainName(_ data: Data, offset: Int) -> (String, Int)? { + var labels: [String] = [] + var index = offset + while index < data.count { + let length = Int(data[index]) + if length == 0 { + index += 1 + break + } + guard index + length < data.count else { return nil } + let labelData = data.subdata(in: index+1..<(index+1+length)) + if let label = String(data: labelData, encoding: .utf8) { + labels.append(label) + } else { + return nil + } + index += length + 1 + } + return (labels.joined(separator: "."), index) + } +} + diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift new file mode 100644 index 0000000..9958009 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/ExtensionsPreferencesManager.swift @@ -0,0 +1,24 @@ +// +// ExtensionsPreferencesManager.swift +// SelfControl +// +// Created by Satendra Singh on 12/07/25. +// + +import Foundation +import os.log + +final class ExtensionsPreferencesManager { + let appGroup = "X6FQ433AWK.com.application.SelfControl.Extension" + var blockedDomains: [String] = [] + lazy var defaults = UserDefaults(suiteName: appGroup) + + init(blockedDomains: [String]) { + self.blockedDomains = blockedDomains + } + + init () { + blockedDomains = defaults?.stringArray(forKey: "BlockedDomains") ?? [] + os_log("SC] 🔍 DNS Blocked Domains Loaded: %{public}@", blockedDomains) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift new file mode 100644 index 0000000..f652ce5 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift @@ -0,0 +1,279 @@ +import NetworkExtension +import os.log +import dnssd + +/// FilterDataProvider is a NEFilterDataProvider subclass that intercepts flows and applies a test rule. +class FilterDataProvider: NEFilterDataProvider { + // MARK: - Properties +// private let listner = PlistListner() + /// A dictionary for storing flows related to the same process. + private var relatedFlows: [String: [NEFilterSocketFlow]] = [:] + + // MARK: - Initialization + + override init() { + os_log("[SC] 🔍] FilterDataProvider: init") + super.init() +// listner.startListening() + } + + // MARK: - Filter Lifecycle + static let localPort = "8888" + + override func startFilter(completionHandler: @escaping (Error?) -> Void) { + os_log("[SC] 🔍] FilterDataProvider: Starting filter", log: OSLog.default, type: .info) + os_log("[SC] 🔍] captrued Domains: %{public}@", log: OSLog.default, type: .error, ExtensionsPreferencesManager().blockedDomains) +// os_log("[SC] 🔍] captrued blockedUrls: %{public}@", log: OSLog.default, type: .error, IPCConnection.shared.blockedUrls) + + // Filter incoming TCP connections on port 8888 + let blockedHosts = IPCConnection.shared.blockedUrls + +// let filterRules = blockedHosts.map { address -> NEFilterRule in +// // let localNetwork = NWHostEndpoint(hostname: address as! String, port: FilterDataProvider.localPort) +// let inboundNetworkRule = NENetworkRule(remoteNetwork: address, +// remotePrefix: 0, +// localNetwork: nil, +// localPrefix: 0, +// protocol: .any, +// direction: .outbound) +// return NEFilterRule(networkRule: inboundNetworkRule, action: .filterData) +// } + // Filter incoming TCP connections on port 8888 +// let filterRules = ["0.0.0.0", "::"].map { address -> NEFilterRule in +// let filterRules = ["*"].map { address -> NEFilterRule in +// let localNetwork = NWHostEndpoint(hostname: address, port: "*") +// let inboundNetworkRule = NENetworkRule(remoteNetwork: nil, +// remotePrefix: 0, +// localNetwork: localNetwork, +// localPrefix: 0, +// protocol: .any, +// direction: .outbound) +// return NEFilterRule(networkRule: inboundNetworkRule, action: .filterData) +// } +// + let filterRules = blockedHosts.map { address -> NEFilterRule in + let localNetwork = NWHostEndpoint(hostname: address, port: "*") + let inboundNetworkRule = NENetworkRule(remoteNetwork: nil, + remotePrefix: 0, + localNetwork: localNetwork, + localPrefix: 0, + protocol: .any, + direction: .outbound) + return NEFilterRule(networkRule: inboundNetworkRule, action: .filterData) + } + + + // Create a rule matching all outbound traffic. +// let networkRule = NENetworkRule(remoteNetwork: nil, +// remotePrefix: 0, +// localNetwork: nil, +// localPrefix: 0, +// protocol: .any, +// direction: .outbound) +// let filterRule = NEFilterRule(networkRule: networkRule, action: .filterData) +// let filterSettings = NEFilterSettings(rules: [filterRule], defaultAction: .allow) + let filterSettings = NEFilterSettings(rules: filterRules, defaultAction: .allow) + + apply(filterSettings) { error in + if let error = error { + os_log("[SC] 🔍] Error applying filter settings: %@", log: OSLog.default, type: .error, error.localizedDescription) + } + completionHandler(error) + } + } + + override func stopFilter(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + os_log("[SC] 🔍] FilterDataProvider: Stopping filter with reason %d", log: OSLog.default, type: .info, reason.rawValue) + completionHandler() + } + + // MARK: - Flow Handling + + // Called for each new flow. + override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict { + os_log("[SC] 🔍] FilterDataProvider: handleNewFlow invoked", log: OSLog.default, type: .debug) +// if let appID = flow.sourceAppIdentifier { +// print("App making the request: \(appID)") +// } + guard let socketFlow = flow as? NEFilterSocketFlow else { + os_log("[SC] 🔍] Not a socket flow. Allowing.", log: OSLog.default, type: .info) + return .allow() + } + + // Extract remote endpoint (if available). + guard let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint else { + os_log("[SC] 🔍] No valid remote endpoint. Allowing flow.", log: OSLog.default, type: .error) + return .allow() + } + os_log("[SC] 🔍] Flow from remote endpoint: %{public}@, URL: %{public}@", log: OSLog.default, type: .debug, remoteEndpoint.description, flow.url?.description ?? "nil") + if let urlString = flow.url?.absoluteString { + os_log("[SC] 🔍] Checking URL path: %{public}@", urlString) +// let blockedHosts = ["google.com/mail", "google.com/news", "facebook.com"] + let blockedHosts = IPCConnection.shared.blockedUrls + os_log("[SC] 🔍] BlockedList: %{public}@ checking:%{public}@",blockedHosts, urlString) + for host in blockedHosts { + if urlString.contains(host) { + os_log("[SC] 🔍] Blocking flow to handleNewFlow %{public}@", urlString) + return .drop() +// return .allow() + } + } + } + // if blockedHosts.contains(flow.url?.path() ?? "") { + // os_log("Blocking flow to %@", remoteEndpoint.hostname) + // return .drop() + // } + // Only process outbound traffic. + if socketFlow.direction != .outbound { + os_log("[SC] 🔍] Non-outbound traffic. Allowing.", log: OSLog.default, type: .info) + return .allow() + } + + return .allow() + + // Process the flow and decide a verdict. + let verdict = processEvent(for: socketFlow) + os_log("[SC] 🔍] Verdict for flow: %{public}@", log: OSLog.default, type: .debug, verdict.debugDescription) + return verdict + } + +// override func handleInboundDataComplete(for flow: NEFilterFlow) -> NEFilterDataVerdict { +// if let socketFlow = flow as? NEFilterSocketFlow { +// if let data = socketFlow.readData { +// if let requestString = String(data: data, encoding: .utf8) { +// if requestString.hasPrefix("GET") || requestString.hasPrefix("POST") { +// print("Request: \(requestString)") +// // Parse the path here +// } +// } +// } +// } +// return .allow() +// } +// +// override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict { +// return .filterDataVerdict(withFilterInbound: true, +// peekInboundBytes: 1024, +// filterOutbound: true, +// peekOutboundBytes: 1024) +// } + + + override func handleInboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict { + if let requestString = String(data: readBytes, encoding: .utf8) { + print("Outbound data: \(requestString)") + os_log("[SC] 🔍] handleInboundData: %{public}@", requestString) + + if requestString.contains("facebook.com/friends") { + return .drop() + } + } + return .allow() + } + + override func displayMessage(_ message: String, completionHandler: @escaping (Bool) -> Void) { + return completionHandler(true) + } + + override func handleOutboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict { + if let urlString = flow.url?.absoluteString { + os_log("[SC] 🔍] handleOutboundData URL: %{public}@", urlString) + if urlString.contains("facebook.com/friends") { + return .drop() + } + } + + if let requestString = String(data: readBytes, encoding: .utf8) { + print("Outbound data: \(requestString)") + os_log("[SC] 🔍] handleOutboundData: %{public}@", requestString) + + if requestString.contains("facebook.com/friends") { + return .drop() + } + } + return .allow() + } + +// override func handleOutboundData(from flow: NEFilterFlow, +// readBytesStartOffset offset: Int, +// readBytes: Data, +// completionHandler: @escaping (NEFilterDataVerdict) -> Void) { +// +// if let requestString = String(data: readBytes, encoding: .utf8) { +// print("Outbound data: \(requestString)") +// +// if requestString.contains("facebook.com") { +// completionHandler(.drop()) +// return +// } +// } +// +// // If undecided, ask for more data +// completionHandler(.allow()) +// } + + /// Processes the flow and returns a verdict. + /// This is a simplified test rule that blocks flows destined for "example.com". + private func processEvent(for flow: NEFilterSocketFlow) -> NEFilterNewFlowVerdict { + guard let endpoint = flow.remoteEndpoint as? NWHostEndpoint else { + return .allow() + } + os_log("[SC] 🔍] processEvent endpoint.hostname: %{public}@ ", endpoint.hostname) + os_log("[SC] 🔍] processEvent remoteHostname: %{public}@ ", flow.remoteHostname ?? "NOTHING") + guard let host = flow.remoteHostname?.lowercased().domainString else { + os_log("[SC] 🔍] processEvent No Host") + return .allow() + } +// os_log("[SC] 🔍] This is localFlowEndpoint: %{public}@ ", flow.localFlowEndpoint?.debugDescription ?? "NOTHING") + let blockedHosts = IPCConnection.shared.blockedUrls + +// if host == "google.com" || host == "8.8.8.8" { + if blockedHosts.contains(host) { + os_log("[SC] 🔍] processEvent: Blocking flow to processEvent %{public}@ ", host) + return .drop() +// return .allow() + } + // Optionally log other flows for debugging + os_log("[SC] 🔍] processEvent: Allowing flow to %{public}@", log: OSLog.default, type: .info, host) + return .allow() + } + + + // MARK: - (Optional) Handling Related Flows & Alerts + + /// Adds a flow to a list of related flows for a given key. + private func addRelatedFlow(forKey key: String, flow: NEFilterSocketFlow) { + os_log("[SC] 🔍] Adding related flow for key: %@", log: OSLog.default, type: .debug, key) + if relatedFlows[key] == nil { + relatedFlows[key] = [] + } + relatedFlows[key]?.append(flow) + } + + /// Processes related flows once a decision is made for a given key. + private func processRelatedFlows(forKey key: String) { + guard let flows = relatedFlows[key] else { + os_log("[SC] 🔍] No related flows for key: %@", log: OSLog.default, type: .debug, key) + return + } + for flow in flows { + let verdict = processEvent(for: flow) + resumeFlow(flow, with: verdict) + } + relatedFlows[key] = nil + } + + /// A stub method for resuming a flow with a verdict. + private func resumeFlow(_ flow: NEFilterSocketFlow, with verdict: NEFilterNewFlowVerdict) { + // In a complete implementation, this would resume the paused flow with the provided verdict. + os_log("[SC] 🔍] Resuming flow %@ with verdict %@", log: OSLog.default, type: .info, flow.debugDescription, verdict.debugDescription) + } + + /// A stub method to simulate alerting the user. + /// In a complete implementation, this might trigger an IPC to your app for user intervention. + private func alertUser(for flow: NEFilterSocketFlow) { + os_log("[SC] 🔍] Alert: User decision needed for flow %@", log: OSLog.default, type: .info, flow.debugDescription) + } + } +//https://developer.chrome.com/docs/extensions/how-to/distribute/install-extensions + diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift new file mode 100644 index 0000000..79ebcc5 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift @@ -0,0 +1,198 @@ +/* + See the LICENSE.txt file for this sample’s licensing information. + + Abstract: + This file contains the implementation of the app <-> provider IPC connection + */ + +import Foundation +import os.log +import Network + +/// App --> Provider IPC +@objc protocol ProviderCommunication { + func register(_ completionHandler: @escaping (Bool) -> Void) + func setBlockedURLs(_ urls: [String]) +} + +/// Provider --> App IPC +@objc protocol AppCommunication { + + func promptUser(aboutFlow flowInfo: [String: String], responseHandler: @escaping (Bool) -> Void) + func didSetUrls() +} + +enum FlowInfoKey: String { + case localPort + case remoteAddress +} + +/// The IPCConnection class is used by both the app and the system extension to communicate with each other +class IPCConnection: NSObject { + + // MARK: Properties + + var listener: NSXPCListener? + var currentConnection: NSXPCConnection? + weak var delegate: AppCommunication? + static let shared = IPCConnection() +// var blockedUrls: [String] = ProxyPreferences.getBlockedDomains() + var blockedUrls: [String] = [String]() + + // MARK: Methods + + /** + The NetworkExtension framework registers a Mach service with the name in the system extension's NEMachServiceName Info.plist key. + The Mach service name must be prefixed with one of the app groups in the system extension's com.apple.security.application-groups entitlement. + Any process in the same app group can use the Mach service to communicate with the system extension. + */ + private func extensionMachServiceName(from bundle: Bundle) -> String { + + guard let networkExtensionKeys = bundle.object(forInfoDictionaryKey: "NetworkExtension") as? [String: Any], + let machServiceName = networkExtensionKeys["NEMachServiceName"] as? String else { + fatalError("Mach service name is missing from the Info.plist") + } + + return machServiceName + } + + func startListener() { + + let machServiceName = extensionMachServiceName(from: Bundle.main) + os_log("[SC] 🔍] Starting XPC listener for mach service %@", machServiceName) + + let newListener = NSXPCListener(machServiceName: machServiceName) + newListener.delegate = self + newListener.resume() + listener = newListener + } + + /// This method is called by the app to register with the provider running in the system extension. + func register(withExtension bundle: Bundle, delegate: AppCommunication, completionHandler: @escaping (Bool) -> Void) { + + self.delegate = delegate + + guard currentConnection == nil else { + os_log("[SC] 🔍] Already registered with the provider") + completionHandler(true) + return + } + + let machServiceName = extensionMachServiceName(from: bundle) + let newConnection = NSXPCConnection(machServiceName: machServiceName, options: []) + + // The exported object is the delegate. + newConnection.exportedInterface = NSXPCInterface(with: AppCommunication.self) + newConnection.exportedObject = delegate + + // The remote object is the provider's IPCConnection instance. + newConnection.remoteObjectInterface = NSXPCInterface(with: ProviderCommunication.self) + + currentConnection = newConnection + newConnection.resume() + + guard let providerProxy = newConnection.remoteObjectProxyWithErrorHandler({ registerError in + os_log("[SC] 🔍] Failed to register with the provider: %@", registerError.localizedDescription) + self.currentConnection?.invalidate() + self.currentConnection = nil + completionHandler(false) + }) as? ProviderCommunication else { + fatalError("Failed to create a remote object proxy for the provider") + } + providerProxy.register(completionHandler) +// providerProxy.setBlockedURLs(blockedUrls) + } + + /** + This method is called by the provider to cause the app (if it is registered) to display a prompt to the user asking + for a decision about a connection. + */ + func promptUser(aboutFlow flowInfo: [String: String], responseHandler:@escaping (Bool) -> Void) -> Bool { + + guard let connection = currentConnection else { + os_log("[SC] 🔍] Cannot prompt user because the app isn't registered") + return false + } + + guard let appProxy = connection.remoteObjectProxyWithErrorHandler({ promptError in + os_log("[SC] 🔍] Failed to prompt the user: %@", promptError.localizedDescription) + self.currentConnection = nil + responseHandler(true) + }) as? AppCommunication else { + fatalError("Failed to create a remote object proxy for the app") + } + + appProxy.promptUser(aboutFlow: flowInfo, responseHandler: responseHandler) + + return true + } +} + +extension IPCConnection: NSXPCListenerDelegate { + + // MARK: NSXPCListenerDelegate + + func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { + + // The exported object is this IPCConnection instance. + newConnection.exportedInterface = NSXPCInterface(with: ProviderCommunication.self) + newConnection.exportedObject = self + + // The remote object is the delegate of the app's IPCConnection instance. + newConnection.remoteObjectInterface = NSXPCInterface(with: AppCommunication.self) + + newConnection.invalidationHandler = { + self.currentConnection = nil + } + + newConnection.interruptionHandler = { + self.currentConnection = nil + } + + currentConnection = newConnection + newConnection.resume() + + return true + } + + func enableURLBlocking(_ urls: [String]) { + os_log("[SC] 🔍] Enabling URL blocking") + guard let providerProxy = currentConnection?.remoteObjectProxyWithErrorHandler({ registerError in + os_log("[SC] 🔍] Failed to register with the provider: %@", registerError.localizedDescription) + }) as? ProviderCommunication else { + fatalError("Failed to create a remote object proxy for the provider") + } + providerProxy.setBlockedURLs(urls) + } +} + +extension IPCConnection: ProviderCommunication { + func setBlockedURLs(_ urls: [String]) { + os_log("[SC] 🔍] Blocking: %{public}@",urls) + blockedUrls = urls +// delegate?.didSetUrls() + + guard let connection = currentConnection else { + print("[SC] 🔍] Cannot update blocked urls, app isn't registered") + return + } + + guard let appProxy = connection.remoteObjectProxyWithErrorHandler({ promptError in + os_log("[SC] 🔍] Failed to prompt the user: %@", promptError.localizedDescription) +// self.currentConnection = nil +// responseHandler(true) + }) as? AppCommunication else { + fatalError("Failed to create a remote object proxy for the app") + } + appProxy.didSetUrls() + } + + + // MARK: ProviderCommunication + + func register(_ completionHandler: @escaping (Bool) -> Void) { + + os_log("[SC] 🔍] App registered") + completionHandler(true) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist b/Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist new file mode 100644 index 0000000..cf77fee --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Info.plist @@ -0,0 +1,16 @@ + + + + + NetworkExtension + + NEMachServiceName + $(TeamIdentifierPrefix)com.application.SelfControl.corebits.network + NEProviderClasses + + com.apple.networkextension.filter-data + $(PRODUCT_MODULE_NAME).FilterDataProvider + + + + diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift new file mode 100644 index 0000000..d31f946 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/PlistListner.swift @@ -0,0 +1,58 @@ +// +// PlistListner.swift +// SelfControlExtension +// +// Created by Satendra Singh on 16/08/25. +// + +import Foundation +import Network +import os.log + +final class PlistListner: NSObject, ObservableObject { + + var listener: NWListener? = try! NWListener(using: .tcp, on: 8080) + var blockeddomainFetcher: (() -> [String])? + + func startListening() { + os_log("[SC] 🔍] NW startListening") +// let objects: [[String: Any]] = [ +// ["id": 1, "name": "Alice", "status": "running"], +// ["id": 2, "name": "Bob", "status": "stopped"], +// ["id": 3, "name": "Charlie", "status": "idle"] +// ] + listener = try! NWListener(using: .tcp, on: 8080) + listener?.newConnectionHandler = { conn in + let blockedDomainList: [String] = self.blockeddomainFetcher?() ?? [] + let blockedUrls = ["blocked": blockedDomainList] + let jsonData = try! JSONSerialization.data(withJSONObject: blockedUrls, options: []) + let jsonString = String(data: jsonData, encoding: .utf8)! + + os_log("[SC] 🔍] NW newConnectionHandler") + conn.start(queue: DispatchQueue.global(qos: .userInitiated)) +// conn.receiveMessage { data, _, _, _ in + os_log("[SC] 🔍] NW conn.receiveMessage") + let response = """ + HTTP/1.1 200 OK\r + Content-Type: application/json\r + Access-Control-Allow-Origin: *\r + \r + \(jsonString) + """ + conn.send(content: response.data(using: .utf8), contentContext: .finalMessage , completion: .contentProcessed { error in + os_log("[SC] 🔍] NW Sent response:\(error)") +// conn.cancel() + + }) +// } + + conn.stateUpdateHandler = { state in + if state == .ready { + os_log("[SC] 🔍] NW stateUpdateHandler ready") + } + } + } + listener?.start(queue: DispatchQueue.global(qos: .userInitiated)) +// RunLoop.main.run() + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/README.md b/Self-Control-Extension/SelfControl/SelfControlExtension/README.md new file mode 100644 index 0000000..234977c --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/README.md @@ -0,0 +1,8 @@ +# SelfControlExtension + +This directory contains the NetworkExtension implementation for SelfControl, responsible for URL and domain filtering. + +## Structure + +- `FilterProvider/` - NetworkExtension filter provider implementation +- `Configuration/` - Extension configuration code \ No newline at end of file diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements b/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements new file mode 100644 index 0000000..969c744 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.developer.networking.networkextension + + content-filter-provider + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix)com.application.SelfControl.corebits + + com.apple.security.network.server + + + diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift new file mode 100644 index 0000000..b2758df --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/String+URL.swift @@ -0,0 +1,35 @@ +// +// String+URL.swift +// SelfControlExtension +// +// Created by Satendra Singh on 05/08/25. +// + +import Foundation + +extension String { + var domainString: String? { + Self.extractDomain(from: self) + } + + static func extractDomain(from urlString: String) -> String? { + var formattedURLString = urlString + if !formattedURLString.lowercased().hasPrefix("http://") && + !formattedURLString.lowercased().hasPrefix("https://") { + formattedURLString = "https://" + formattedURLString + } + + guard let url = URL(string: formattedURLString), + let host = url.host else { + return nil + } + + // Optional: extract root domain (e.g., "facebook.com" from "sub.facebook.com") + let components = host.components(separatedBy: ".") + if components.count >= 2 { + return components.suffix(2).joined(separator: ".") + } else { + return host + } + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/main.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/main.swift new file mode 100644 index 0000000..ffadae7 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/main.swift @@ -0,0 +1,18 @@ +// +// main.swift +// SelfControlExtension +// +// Created by Egzon Arifi on 02/04/2025. +// + +import Foundation +import NetworkExtension +import os.log + +autoreleasepool { + os_log("[SC] 🔍] first light") + NEProvider.startSystemExtensionMode() + IPCConnection.shared.startListener() +} + +dispatchMain() From 95450af80b6ecc9bc8c80560a5583c8d60c6beb2 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Tue, 21 Oct 2025 16:38:59 +0530 Subject: [PATCH 2/9] implemented network extension and safari extensions --- .../Constants.swift | 13 + ...ontentBlockerExtensionRequestHandler.swift | 90 +++ .../ContentBlockerRequestHandler.swift | 66 ++ .../SelfControl Safari Extension/Info.plist | 15 + .../SelfControl Safari Extension.entitlements | 10 + .../blockerList.json | 60 ++ .../SelfControl.xcodeproj/project.pbxproj | 197 ++++- .../SelfControl/SelfControl/Info.plist | 4 +- .../SelfControl/Main/ContentView.swift | 8 + .../SelfControl/Main/DNSResolver.swift | 125 +++ .../SelfControl/Main/FilterViewModel.swift | 29 +- .../Preferences/PreferencesView.swift | 5 +- .../SafariExtension/BlockListManager.swift | 96 +++ .../SafariExtension/SafariExtensionView.swift | 57 ++ .../SimpleRegexConverter.swift | 103 +++ .../SelfControl/SelfControl.entitlements | 2 + .../SelfControl/SelfControlApp.swift | 94 ++- .../SelfControlExtension/Constants.swift | 35 + .../FilterDataProvider.swift | 148 +++- .../HostToIpMapping.swift | 82 ++ .../SelfControlExtension/IPCConnection.swift | 23 +- .../SelfControlExtension/Process/Binary.swift | 175 +++++ .../Process/Process.swift | 330 ++++++++ .../Process/SourceAppAuditTokenBuilder.swift | 61 ++ .../Process/Utilities.swift | 714 ++++++++++++++++++ .../ReverseDomainMapper.swift | 57 ++ .../SelfControlExtension/TLDURLToDomain.swift | 49 ++ 27 files changed, 2608 insertions(+), 40 deletions(-) create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json create mode 100644 Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift new file mode 100644 index 0000000..58dd76f --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Constants.swift @@ -0,0 +1,13 @@ +// +// Constants.swift +// SelfControl +// +// Created by Satendra Singh on 09/10/25. +// + + + +enum Constants { + /// File name for the JSON file with Safari rules. + static let SAFARI_BLOCKER_FILE_NAME = "blockerList.json" +} diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift new file mode 100644 index 0000000..9c4ba46 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift @@ -0,0 +1,90 @@ +// +// ContentBlockerExtensionRequestHandler.swift +// SelfControl +// +// Created by Satendra Singh on 09/10/25. +// + +import os.log +import Foundation + +public enum ContentBlockerExtensionRequestHandler { + /// Handles content blocking extension request for rules. + /// + /// This method loads the content blocker rules JSON file from the shared container + /// and attaches it to the extension context to be used by Safari. + /// + /// - Parameters: + /// - context: The extension context that initiated the request. + /// - groupIdentifier: The app group identifier used to access the shared container. + public static func handleRequest(with context: NSExtensionContext, groupIdentifier: String) { + os_log(.info, "Start loading the content blocker") + + // Get the shared container URL using the provided group identifier + guard + let appGroupURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: groupIdentifier + ) + else { + context.cancelRequest( + withError: createError(code: 1001, message: "Failed to access App Group container.") + ) + return + } + + // Construct the path to the shared blocker list file + let sharedFileURL = appGroupURL.appendingPathComponent(Constants.SAFARI_BLOCKER_FILE_NAME) + + // Determine which blocker list file to use + var blockerListFileURL = sharedFileURL + if !FileManager.default.fileExists(atPath: sharedFileURL.path) { + os_log(.info, "No blocker list file found. Using the default one.") + + // Fall back to the default blocker list included in the bundle + guard + let defaultURL = Bundle.main.url(forResource: "blockerList", withExtension: "json") + else { + context.cancelRequest( + withError: createError( + code: 1002, + message: "Failed to find default blocker list." + ) + ) + return + } + blockerListFileURL = defaultURL + } + + // Create an attachment with the blocker list file + guard let attachment = NSItemProvider(contentsOf: blockerListFileURL) else { + context.cancelRequest( + withError: createError(code: 1003, message: "Failed to create attachment.") + ) + return + } + + // Prepare and complete the extension request with the blocker list + let item = NSExtensionItem() + item.attachments = [attachment] + + context.completeRequest( + returningItems: [item] + ) { _ in + os_log(.info, "Finished loading the content blocker") + } + } + + /// Creates an NSError with the specified code and message. + /// + /// - Parameters: + /// - code: The error code. + /// - message: The error message. + /// - Returns: An NSError object with the specified parameters. + private static func createError(code: Int, message: String) -> NSError { + return NSError( + domain: "extension request handler", + code: code, + userInfo: [NSLocalizedDescriptionKey: message] + ) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift new file mode 100644 index 0000000..90a5176 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift @@ -0,0 +1,66 @@ +// +// ContentBlockerRequestHandler.swift +// SelfControl Safari Extension +// +// Created by Satendra Singh on 07/10/25. +// + +import Foundation +import UniformTypeIdentifiers + +class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling { + // Must match the App Group used by the host app + private let appGroup = "group.com.application.SelfControl.corebits" + private let sharedFileName = "blockerList.json" + + func beginRequest(with context: NSExtensionContext) { + ContentBlockerExtensionRequestHandler.handleRequest(with: context, groupIdentifier: appGroup) + return + let item = NSExtensionItem() + + // Try to load rules from the shared App Group container first + if let sharedFileURL = sharedRulesFileURL(), FileManager.default.fileExists(atPath: sharedFileURL.path) { + let provider = NSItemProvider(contentsOf: sharedFileURL)! + item.attachments = [provider] + context.completeRequest(returningItems: [item], completionHandler: nil) + return + } + + // Fallback: load bundled blockerList.json from the extension resources + if let bundledURL = Bundle.main.url(forResource: "blockerList", withExtension: "json") { + let provider = NSItemProvider(contentsOf: bundledURL)! + item.attachments = [provider] + context.completeRequest(returningItems: [item], completionHandler: nil) + return + } + + // If neither exists, return an empty rules array to avoid errors +// let emptyRulesData = Data("[]".utf8) +// let tmpURL = writeTempData(emptyRulesData, suggestedName: sharedFileName) +// let provider = NSItemProvider(contentsOf: tmpURL)! +// item.attachments = [provider] +// context.completeRequest(returningItems: [item], completionHandler: nil) + } + + // MARK: - Helpers + + private func sharedRulesFileURL() -> URL? { + let fileManager = FileManager.default + guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + NSLog("❌ App Group container not found for %@", appGroup) + return nil + } + return containerURL.appendingPathComponent(sharedFileName) + } + + private func writeTempData(_ data: Data, suggestedName: String) -> URL { + let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let url = tmpDir.appendingPathComponent(suggestedName) + do { + try data.write(to: url, options: .atomic) + } catch { + NSLog("❌ Failed to write temporary rules file: \(error.localizedDescription)") + } + return url + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist new file mode 100644 index 0000000..0d1cc76 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist @@ -0,0 +1,15 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.Safari.content-blocker + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ContentBlockerRequestHandler + + NSHumanReadableDescription + Add a description of what your extension does here. + + diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements new file mode 100644 index 0000000..ab2dad3 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/SelfControl Safari Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.application.SelfControl.corebits + + + diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json new file mode 100644 index 0000000..ea0ed75 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/blockerList.json @@ -0,0 +1,60 @@ +[ + { + "action": { + "type": "block" + }, + "trigger": { + "url-filter": "facebook.com/friends" + } + }, + { + "action": { + "type": "block" + }, + "trigger": { + "url-filter": "facebook.com/marketplace" + } + }, + { + "action": { + "type": "block" + }, + "trigger": { + "url-filter": "facebook.com/watch" + } + } +] +//[ +// { +// "trigger" : { +// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/friends.*" +// }, +// "action" : { +// "type" : "block" +// } +// }, +// { +// "trigger" : { +// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/mynetwork.*" +// }, +// "action" : { +// "type" : "block" +// } +// }, +// { +// "trigger" : { +// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/jobs.*" +// }, +// "action" : { +// "type" : "block" +// } +// }, +// { +// "trigger" : { +// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/marketplace.*" +// }, +// "action" : { +// "type" : "block" +// } +// } +//] diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj index 9f18791..088b18a 100644 --- a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj +++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 77BA53092D9D5E2A00863476 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */; }; 9A441CE32E36024E00A521CC /* com.application.SelfControl.corebits.network.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9A7FFB602E950837007D4A0D /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A7FFA0B2E91950D007D4A0D /* Cocoa.framework */; }; + 9A7FFB692E950837007D4A0D /* SelfControl Safari Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -19,9 +21,27 @@ remoteGlobalIDString = 77BA53052D9D5E2A00863476; remoteInfo = SelfControlExtension; }; + 9A7FFB672E950837007D4A0D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 77BA52CE2D9D5D7700863476 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A7FFB5E2E950837007D4A0D; + remoteInfo = "SelfControl Safari Extension"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 9A7FFA162E91950D007D4A0D /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 9A7FFB692E950837007D4A0D /* SelfControl Safari Extension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; 9AFC129E2E355671003B5455 /* Embed System Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -39,6 +59,8 @@ 77BA52D62D9D5D7700863476 /* SelfControl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SelfControl.app; sourceTree = BUILT_PRODUCTS_DIR; }; 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = com.application.SelfControl.corebits.network.systemextension; sourceTree = BUILT_PRODUCTS_DIR; }; 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; + 9A7FFA0B2E91950D007D4A0D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SelfControl Safari Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -61,12 +83,20 @@ 9A6873762E2814B30054AD3F /* Exceptions for "SelfControlExtension" folder in "SelfControl" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + BlockOrAllowList.swift, IPCConnection.swift, PlistListner.swift, "String+URL.swift", ); target = 77BA52D52D9D5D7700863476 /* SelfControl */; }; + 9A7FFB6D2E950837007D4A0D /* Exceptions for "SelfControl Safari Extension" folder in "SelfControl Safari Extension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -87,6 +117,14 @@ path = SelfControlExtension; sourceTree = ""; }; + 9A7FFB612E950837007D4A0D /* SelfControl Safari Extension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 9A7FFB6D2E950837007D4A0D /* Exceptions for "SelfControl Safari Extension" folder in "SelfControl Safari Extension" target */, + ); + path = "SelfControl Safari Extension"; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -105,6 +143,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9A7FFB5C2E950837007D4A0D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9A7FFB602E950837007D4A0D /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -113,6 +159,7 @@ children = ( 77BA52D82D9D5D7700863476 /* SelfControl */, 77BA530A2D9D5E2A00863476 /* SelfControlExtension */, + 9A7FFB612E950837007D4A0D /* SelfControl Safari Extension */, 77BA53072D9D5E2A00863476 /* Frameworks */, 77BA52D72D9D5D7700863476 /* Products */, ); @@ -123,6 +170,7 @@ children = ( 77BA52D62D9D5D7700863476 /* SelfControl.app */, 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */, + 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */, ); name = Products; sourceTree = ""; @@ -131,6 +179,7 @@ isa = PBXGroup; children = ( 77BA53082D9D5E2A00863476 /* NetworkExtension.framework */, + 9A7FFA0B2E91950D007D4A0D /* Cocoa.framework */, ); name = Frameworks; sourceTree = ""; @@ -146,11 +195,13 @@ 77BA52D32D9D5D7700863476 /* Frameworks */, 77BA52D42D9D5D7700863476 /* Resources */, 9AFC129E2E355671003B5455 /* Embed System Extensions */, + 9A7FFA162E91950D007D4A0D /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( 9A441CE22E36024300A521CC /* PBXTargetDependency */, + 9A7FFB682E950837007D4A0D /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 77BA52D82D9D5D7700863476 /* SelfControl */, @@ -184,6 +235,28 @@ productReference = 77BA53062D9D5E2A00863476 /* com.application.SelfControl.corebits.network.systemextension */; productType = "com.apple.product-type.system-extension"; }; + 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9A7FFB6A2E950837007D4A0D /* Build configuration list for PBXNativeTarget "SelfControl Safari Extension" */; + buildPhases = ( + 9A7FFB5B2E950837007D4A0D /* Sources */, + 9A7FFB5C2E950837007D4A0D /* Frameworks */, + 9A7FFB5D2E950837007D4A0D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 9A7FFB612E950837007D4A0D /* SelfControl Safari Extension */, + ); + name = "SelfControl Safari Extension"; + packageProductDependencies = ( + ); + productName = "SelfControl Safari Extension"; + productReference = 9A7FFB5F2E950837007D4A0D /* SelfControl Safari Extension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -191,7 +264,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1620; + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1620; TargetAttributes = { 77BA52D52D9D5D7700863476 = { @@ -200,6 +273,9 @@ 77BA53052D9D5E2A00863476 = { CreatedOnToolsVersion = 16.2; }; + 9A7FFB5E2E950837007D4A0D = { + CreatedOnToolsVersion = 26.0; + }; }; }; buildConfigurationList = 77BA52D12D9D5D7700863476 /* Build configuration list for PBXProject "SelfControl" */; @@ -218,6 +294,7 @@ targets = ( 77BA52D52D9D5D7700863476 /* SelfControl */, 77BA53052D9D5E2A00863476 /* SelfControlExtension */, + 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */, ); }; /* End PBXProject section */ @@ -237,6 +314,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9A7FFB5D2E950837007D4A0D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -254,6 +338,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 9A7FFB5B2E950837007D4A0D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -262,6 +353,11 @@ target = 77BA53052D9D5E2A00863476 /* SelfControlExtension */; targetProxy = 9A441CE12E36024300A521CC /* PBXContainerItemProxy */; }; + 9A7FFB682E950837007D4A0D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */; + targetProxy = 9A7FFB672E950837007D4A0D /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -394,8 +490,19 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\""; DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SelfControl/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -423,8 +530,19 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\""; DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SelfControl/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -462,6 +580,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "-lbsm"; PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network; PRODUCT_NAME = "$(inherited)"; SKIP_INSTALL = YES; @@ -491,6 +610,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 13.5; MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "-lbsm"; PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network; PRODUCT_NAME = "$(inherited)"; SKIP_INSTALL = YES; @@ -499,6 +619,72 @@ }; name = Release; }; + 9A7FFB6B2E950837007D4A0D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "SelfControl Safari Extension/SelfControl Safari Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.application.SelfControl.corebits.SelfControl-Safari-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 9A7FFB6C2E950837007D4A0D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "SelfControl Safari Extension/SelfControl Safari Extension.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X6FQ433AWK; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@executable_path/../../../../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 13.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.application.SelfControl.corebits.SelfControl-Safari-Extension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -529,6 +715,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 9A7FFB6A2E950837007D4A0D /* Build configuration list for PBXNativeTarget "SelfControl Safari Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9A7FFB6B2E950837007D4A0D /* Debug */, + 9A7FFB6C2E950837007D4A0D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 77BA52CE2D9D5D7700863476 /* Project object */; diff --git a/Self-Control-Extension/SelfControl/SelfControl/Info.plist b/Self-Control-Extension/SelfControl/SelfControl/Info.plist index d876c18..b0aaa9d 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/Info.plist +++ b/Self-Control-Extension/SelfControl/SelfControl/Info.plist @@ -24,6 +24,8 @@ $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2025 SelfControl. All rights reserved. + NSLocalNetworkUsageDescription + DNS lookup NSMainStoryboardFile Main NSPrincipalClass @@ -32,7 +34,5 @@ NSSupportsSuddenTermination - NSLocalNetworkUsageDescription - DNS lookup diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift index 7dfa696..a8912aa 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/ContentView.swift @@ -14,6 +14,7 @@ import Cocoa struct ContentView: View { @EnvironmentObject var viewModel: FilterViewModel @Environment(\.openWindow) private var openWindow + @State private var newDomain = "" var body: some View { VStack(spacing: 20) { @@ -51,11 +52,18 @@ struct ContentView: View { } } } + HStack { + TextField("Enter Url to test block", text: $newDomain) + Button("Test Url Blocking") { + viewModel.checkUrlRequest(url: newDomain) + } + } // Button { // viewModel.setBlockedUrls(urls: ProxyPreferences.getBlockedDomains()) // } label: { // Text("Enable Url Blocking") // } + SafariExtensionView() } .padding() .frame(minWidth: 150, minHeight: 150) diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift new file mode 100644 index 0000000..be301c4 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/DNSResolver.swift @@ -0,0 +1,125 @@ +// +// DNSResolver.swift +// SelfControl +// +// Created by Satendra Singh on 21/10/25. +// + + +import Foundation +import Network + +actor DNSResolverActor { + var resolvedIPs: Set = [] + func resolve(hostURL: [String]) async -> Set { + var intendedHosts: Set = [] + for url in hostURL { + print("Resolving: \(url)") + if let host = DNSResolver.getHost(from: url) { + intendedHosts.insert(host) + } + } + + resolvedIPs = [] + for host in intendedHosts { + let ips = await DNSResolver.resolve(hostname: host) + print("host: \(host), ips: \(ips)") + resolvedIPs.formUnion(ips) + } + return resolvedIPs + } +} + +/// A modern async DNS resolver using Network.framework +final class DNSResolver { + static func getHost(from input: String) -> String? { + var string = input + if !string.contains("://") { + string = "https://" + string + } + return URL(string: string)?.host + } + + static func resolve(hostURL: String, timeout: TimeInterval = 5.0) async -> [String] { + guard let host = getHost(from: hostURL) else { return [] } + return await resolve(hostname: host, timeout: timeout) + } + /// Resolves IPv4/IPv6 addresses for a given domain asynchronously. + /// - Parameters: + /// - hostname: The domain name (e.g. "facebook.com"). + /// - timeout: Optional timeout in seconds (default 5s). + /// - Returns: Array of resolved IP addresses as strings. + static func resolve(hostname: String, timeout: TimeInterval = 5.0) async -> [String] { + await withCheckedContinuation { continuation in + let params = NWParameters.tcp + params.allowLocalEndpointReuse = true + + // We use the resolver service to resolve DNS asynchronously + let endpoint = NWEndpoint.hostPort(host: .name(hostname, nil), port: 80) + + let resolver = NWConnection(to: endpoint, using: params) + + var didResume = false + + @Sendable func finish(_ ips: [String]) { + guard !didResume else { return } + didResume = true + continuation.resume(returning: ips) + resolver.cancel() + } + + resolver.stateUpdateHandler = { state in + switch state { + case .ready: + // Once ready, we can extract resolved IPs from the endpoint + if let remote = resolver.currentPath?.remoteEndpoint, + case let .hostPort(host, _) = remote { + switch host { + case .ipv4(let addr): + finish([ipv4ToString(addr)]) + case .ipv6(let addr): + finish([ipv6ToString(addr)]) + default: + finish([]) + } + } else { + finish([]) + } + + case .failed(_): + finish([]) + default: + break + } + } + + // Start resolution + resolver.start(queue: .global(qos: .userInitiated)) + + // Timeout handler + DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { [weak resolver] in + // If still not finished, cancel and return empty + resolver?.cancel() + finish([]) + } + } + } + + /// Converts IPv4 address to string. + nonisolated private static func ipv4ToString(_ addr: IPv4Address) -> String { + let bytes = [UInt8](addr.rawValue) + guard bytes.count == 4 else { return "" } + return bytes.map(String.init).joined(separator: ".") + } + + /// Converts IPv6 address to string. + nonisolated private static func ipv6ToString(_ addr: IPv6Address) -> String { + let bytes = [UInt8](addr.rawValue) + guard bytes.count == 16 else { return "" } + let segments = stride(from: 0, to: bytes.count, by: 2).map { + (UInt16(bytes[$0]) << 8) | UInt16(bytes[$0 + 1]) + } + // Basic hex formatting; does not perform zero-compression (::) + return segments.map { String(format: "%x", $0) }.joined(separator: ":") + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift b/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift index 8f05055..a2d8051 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift +++ b/Self-Control-Extension/SelfControl/SelfControl/Main/FilterViewModel.swift @@ -16,7 +16,8 @@ final class FilterViewModel: NSObject, ObservableObject, OSSystemExtensionReques @State private var domains = ProxyPreferences.getBlockedDomains() private let listner = PlistListner() @Published var delay: Double = 0.0 - + var blockedIPAddressed: [String] = [] + // Date formatter used to log entries lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -84,8 +85,23 @@ final class FilterViewModel: NSObject, ObservableObject, OSSystemExtensionReques func setBlockedUrls(urls: [String]) { IPCConnection.shared.enableURLBlocking(urls) + Task { + let ips: Set = await DNSResolverActor().resolve(hostURL: urls) + print("Resolved app:\(ips)") + setIPAddressesToBlock(addresses: Array(ips)) + } } + func setIPAddressesToBlock(addresses: [String]) { + IPCConnection.shared.enableIPAddressesBlocking(addresses) + } + + private func refreshBlockedIPs() { + self.listner.blockeddomainFetcher = { + return ProxyPreferences.getBlockedDomains() + } + } + func updateStatus() { if NEFilterManager.shared().isEnabled { registerWithProvider() @@ -215,7 +231,15 @@ final class FilterViewModel: NSObject, ObservableObject, OSSystemExtensionReques // } activateExtension() } - + + func checkUrlRequest(url: String) { + URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in + print("Response: \(String(describing: response))") + print("Data: \(String(describing: data))") + print("Error: \(String(describing: error))") + }.resume() + } + func stopFilter() { let filterManager = NEFilterManager.shared() status = .indeterminate @@ -309,3 +333,4 @@ final class FilterViewModel: NSObject, ObservableObject, OSSystemExtensionReques print("didSetUrls+++++") } } + diff --git a/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift b/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift index 6cacbf4..a57e5ed 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift +++ b/Self-Control-Extension/SelfControl/SelfControl/Preferences/PreferencesView.swift @@ -7,7 +7,6 @@ import SwiftUI - struct PreferencesView: View { @State private var domains = ProxyPreferences.getBlockedDomains() @State private var newDomain = "" @@ -50,8 +49,8 @@ struct PreferencesView: View { func addDomain() { guard !newDomain.isEmpty else { return } - guard let domainValue = newDomain.domainString else { return } - domains.append(domainValue) +// guard let domainValue = newDomain.domainString else { return } + domains.append(newDomain) newDomain = "" ProxyPreferences.setBlockedDomains(domains) } diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift new file mode 100644 index 0000000..b12b12a --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListManager.swift @@ -0,0 +1,96 @@ +import Foundation +import SafariServices + +struct BlockRule: Codable { + struct Trigger: Codable { + let urlFilter: String + + enum CodingKeys: String, CodingKey { + case urlFilter = "url-filter" + } + } + + struct Action: Codable { + let type: String + } + + let trigger: Trigger + let action: Action +} + + +enum BlockListManager { + + static func updateSafariBlockList(blockedPaths: [String], appGroup: String, extensionIdentifier: String) { + var rules: [BlockRule] = [] + + for path in blockedPaths { +// let components = path.split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false) +// let domain = String(components[0]) +// let pathPart = components.count == 2 ? String(components[1]) : "" +// +// let escapedDomain = NSRegularExpression.escapedPattern(for: domain) +// var pattern = "^https?://(www\\.)?" + escapedDomain +// if !pathPart.isEmpty { +// let escapedPath = NSRegularExpression.escapedPattern(for: pathPart) +// pattern += "/" + escapedPath +// } +// pattern += ".*" + +// rules.append(BlockRule( +// trigger: .init(urlFilter: SimpleRegexConverter.regexFromURL(path) ?? path), +// action: .init(type: "block") +// )) + rules.append(BlockRule( + trigger: .init(urlFilter: path), + action: .init(type: "block") + )) + } + + do { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(rules) + + let fileManager = FileManager.default + guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + print("❌ App group container not found.") + return + } + + // Ensure folder exists before writing + try fileManager.createDirectory(at: containerURL, withIntermediateDirectories: true, attributes: nil) + + let fileURL = containerURL.appendingPathComponent("blockerList.json") + try data.write(to: fileURL, options: .atomic) + + print("✅ Wrote blockerList.json to: \(fileURL.path)") + + // Query current state for diagnostics; proceed to reload regardless. + SFContentBlockerManager.getStateOfContentBlocker(withIdentifier: extensionIdentifier) { state, error in + if let error = error as NSError? { + print("⚠️ State check error for \(extensionIdentifier): \(error.domain) code \(error.code) – \(error.localizedDescription)") + print("ℹ️ Tips: Ensure the content blocker extension bundle identifier matches exactly, the extension is installed and enabled in Safari, and the app/extension share the same App Group: \(appGroup)") + } else { + print("🔍 Current state of \(extensionIdentifier):", state?.description ?? "Unknown") + if state?.isEnabled == false { + print("⚠️ Extension not enabled in Safari. Please enable it in Settings → Safari → Extensions.") + } + } + + // Attempt reload regardless; this often yields a clearer error if the identifier is wrong. + SFContentBlockerManager.reloadContentBlocker(withIdentifier: extensionIdentifier) { error in + if let error = error as NSError? { + print("❌ Reload error for \(extensionIdentifier): \(error.domain) code \(error.code) – \(error.localizedDescription)") + print("ℹ️ If this persists: verify the extension target’s bundle ID, that the extension is enabled, and that it has access to the shared container.") + } else { + print("✅ Safari Content Blocker reloaded successfully.") + } + } + } + + } catch { + print("❌ Error writing blocker file:", error) + } + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift new file mode 100644 index 0000000..4708094 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SafariExtensionView.swift @@ -0,0 +1,57 @@ +// +// SafariExtensionView.swift +// SelfControl +// +// Created by Satendra Singh on 05/10/25. +// + +import SwiftUI + +struct SafariExtensionView: View { + @StateObject private var vm = BlockListViewModel() + + var body: some View { + VStack { +// HStack { +// Text("Blocked paths (domain/path)").font(.headline) +// Spacer() +// Button(action: vm.addSample) { +// Text("Add sample") +// } +// }.padding(.horizontal) +// +// List { +// ForEach(vm.blockedPaths.indices, id: \ .self) { idx in +// HStack { +// TextField("domain/path", text: $vm.blockedPaths[idx]) +// Button(action: { vm.remove(at: idx) }) { +// Image(systemName: "minus.circle") +// }.buttonStyle(BorderlessButtonStyle()) +// } +// } +// }.frame(minHeight: 200) + + HStack { + Button(action: vm.updateBlocker) { + Text("Update Blocker") + } + Spacer() + Button(action: vm.resetToDefaults) { + Text("Reset Defaults") + } + }.padding() + } + .onAppear { + vm.updateBlocker() + } + .padding() +// .frame(minWidth: 600, minHeight: 320) + } + //1550, 10300, soles, 250, + //50, sanitry, 1, 4, 2 ,6, 5 + //solems, 45, +} + +#Preview { + SafariExtensionView() +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift new file mode 100644 index 0000000..24ffd6c --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/SimpleRegexConverter.swift @@ -0,0 +1,103 @@ +// +// SimpleRegexConverter.swift +// SelfControl +// +// Created by Satendra Singh on 09/10/25. +// + +import Foundation + +final class SimpleRegexConverter { + /// Converts a full URL string into a regex domain pattern suitable for Safari content blocker rules. + /// Example: "https://www.facebook.com/friends" → "https?://(www\\.)?facebook\\.com/.*" + static func regexPattern(from urlString: String) -> String? { + guard let url = buildValidURL(from: urlString), + let host = url.host else { + return nil + } + + // Escape regex special characters in domain + let escapedHost = NSRegularExpression.escapedPattern(for: host) + + // Detect common "www" prefix and make it optional + let pattern: String + if host.hasPrefix("www.") { + let domainWithoutWWW = String(host.dropFirst(4)) + let escapedDomain = NSRegularExpression.escapedPattern(for: domainWithoutWWW) + pattern = "https?://(www\\.)?\(escapedDomain)/.*" + } else { + pattern = "https?://(www\\.)?\(escapedHost)/.*" + } + + return pattern + } + + static func buildValidURL(from path: String) -> URL? { + var trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines) + + // If it's already a valid URL with scheme + if let url = URL(string: trimmedPath), url.scheme != nil { + return url + } + + // Add missing scheme (default to https) + if !trimmedPath.lowercased().hasPrefix("http://") && !trimmedPath.lowercased().hasPrefix("https://") { + trimmedPath = "https://" + trimmedPath + } + + // Try building again + guard var components = URLComponents(string: trimmedPath) else { + return nil + } + + // Fix missing host (e.g., if user entered "example.com/test") + if components.host == nil { + let parts = trimmedPath + .replacingOccurrences(of: "https://", with: "") + .replacingOccurrences(of: "http://", with: "") + .split(separator: "/", maxSplits: 1) + + if let hostPart = parts.first { + components.host = String(hostPart) + components.path = parts.count > 1 ? "/" + parts[1] : "" + } + } + + // Return final URL + return components.url + } + + + static func regexFromURL(_ input: String) -> String? { + var urlString = input.trimmingCharacters(in: .whitespacesAndNewlines) + + // Add a scheme if missing (needed for URL parsing) + if !urlString.lowercased().hasPrefix("http://") && + !urlString.lowercased().hasPrefix("https://") { + urlString = "https://" + urlString + } + + guard let url = URL(string: urlString), let host = url.host else { + return nil + } + + // Escape host and path for regex + let escapedHost = NSRegularExpression.escapedPattern(for: host) + let escapedPath = NSRegularExpression.escapedPattern(for: url.path) + + // Build regex: + // ^[^:]+://+([^.]+\.)*facebook\.com(/friends|[/:]|$) + // - Matches any scheme (http/https) + // - Allows any subdomain levels + // - Matches the path if given + var regex = #"^[^:]+://+([^.]+\.)*\#(escapedHost)"# + + if !escapedPath.isEmpty && escapedPath != "/" { + regex += escapedPath + } + + regex += #"([/:]|$)"# + + return regex + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements index c00a50a..9568570 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements +++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements @@ -4,6 +4,7 @@ com.apple.developer.networking.networkextension + url-filter-provider content-filter-provider com.apple.developer.system-extension.install @@ -12,6 +13,7 @@ com.apple.security.application-groups + group.com.application.SelfControl.corebits $(TeamIdentifierPrefix)com.application.SelfControl.corebits com.apple.security.files.user-selected.read-only diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift b/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift index 2dfb682..e7fa36f 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift +++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControlApp.swift @@ -10,16 +10,90 @@ import SwiftUI @main struct SelfControlApp: App { @StateObject var viewModel = FilterViewModel() - var body: some Scene { - WindowGroup { - ContentView() - .environmentObject(viewModel) // Inject the object into the environment + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(viewModel) // Inject the object into the environment + + Button("Test", action: { + let iPAddress = resolveIPToHostname(ipAddress: "8.8.8.8") + print("IP: \(iPAddress ?? "Unknown")") + // Example: + if let host = reverseDNS(ipAddress: "8.8.8.8") { + print("Hostname: \(host)") + } else { + print("Could not resolve.") + } + // Example + if let domain = reverseDNSUsingGetNameInfo(ipAddress: "163.70.145.35") { + print("Domain: \(domain)") + } else { + print("Reverse DNS lookup failed.") + } + }) + + } + Window("Preferences View", id: "preferences") { + PreferencesView() // Your view to be presented in the new window + .environmentObject(viewModel) // Inject the object into the environment + } + .windowStyle(.automatic) + } + + func resolveIPToHostname(ipAddress: String) -> String? { + let hostRef = CFHostCreateWithName(nil, ipAddress as CFString).takeRetainedValue() + + var resolved: DarwinBoolean = false + if CFHostStartInfoResolution(hostRef, .names, nil) { + if let names = CFHostGetNames(hostRef, &resolved)?.takeUnretainedValue() as NSArray?, + let hostname = names.firstObject as? String { + return hostname + } + } + return nil + } + + func reverseDNS(ipAddress: String) -> String? { + let hostRef = CFHostCreateWithAddress(nil, ipAddressToData(ipAddress) as CFData).takeRetainedValue() + var resolved: DarwinBoolean = false + if CFHostStartInfoResolution(hostRef, .names, nil), + let names = CFHostGetNames(hostRef, &resolved)?.takeUnretainedValue() as NSArray?, + let hostname = names.firstObject as? String { + return hostname + } + return nil + } + + private func ipAddressToData(_ ipAddress: String) -> Data { + var addr = in_addr() + inet_pton(AF_INET, ipAddress, &addr) + return Data(bytes: &addr, count: MemoryLayout.size) } - Window("Preferences View", id: "preferences") { - PreferencesView() // Your view to be presented in the new window - .environmentObject(viewModel) // Inject the object into the environment - } - .windowStyle(.automatic) - } + + func reverseDNSUsingGetNameInfo(ipAddress: String) -> String? { + var addr = sockaddr_in() + addr.sin_len = UInt8(MemoryLayout.size) + addr.sin_family = sa_family_t(AF_INET) + inet_pton(AF_INET, ipAddress, &addr.sin_addr) + + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + + // Precompute length so we don't read from `addr` inside the closure. + let addrLen = socklen_t(addr.sin_len) + + let result: Int32 = withUnsafePointer(to: &addr) { ptr in + ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in + getnameinfo(saPtr, + addrLen, + &hostname, socklen_t(hostname.count), + nil, 0, + NI_NAMEREQD) + } + } + + guard result == 0 else { return nil } + return String(cString: hostname) + } + } diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift new file mode 100644 index 0000000..0db08d6 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Constants.swift @@ -0,0 +1,35 @@ +// +// Constants.swift +// SelfControl +// +// Created by Satendra Singh on 11/10/25. +// + +enum Constants { + /// File name for the JSON file with Safari rules. + static let KEY_UUID = "uuid" + static let KEY_PROCESS_ID = "pid" + static let KEY_PROCESS_ARGS = "args" + static let KEY_PROCESS_NAME = "name" + static let KEY_PROCESS_PATH = "path" + static let KEY_INDEX = "index" + static let KEY_PATH = "paths" + static let KEY_CS_SIGNER = "signatureSigner" + static let KEY_CS_ID = "signatureIdentifier" + static let KEY_CS_INFO = "signingInfo" + static let KEY_CS_AUTHS = "signatureAuthorities" +} + +enum AllowedProcess: String { + case safari = "com.apple.Safari" + case chrome = "com.google.Chrome" + static var allCases: [AllowedProcess] = [.chrome, .safari] + static func isAllowed(_ bundleID: String) -> Bool { + allCases.contains(where: { $0.rawValue == bundleID }) + } + + static func isAllowedProcess(_ process: Process) -> Bool { + let ancestors: [String] = process.ancestors?.value(forKey: "name") as? [String] ?? [] + return allCases.contains(where: { $0.rawValue == process.bundleID }) || ancestors.contains(where: { $0.lowercased().contains("chrome") || $0.lowercased().contains("safari") }) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift index f652ce5..f0ea001 100644 --- a/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift @@ -87,47 +87,141 @@ class FilterDataProvider: NEFilterDataProvider { completionHandler() } - // MARK: - Flow Handling + // MARK: - Flow Handlings + + func reverseDNSLookup(ip: String) -> String? { + var hints = addrinfo(ai_flags: AI_NUMERICHOST, ai_family: AF_UNSPEC, + ai_socktype: SOCK_STREAM, ai_protocol: IPPROTO_TCP, + ai_addrlen: 0, ai_canonname: nil, + ai_addr: nil, ai_next: nil) + var res: UnsafeMutablePointer? + + if getaddrinfo(ip, nil, &hints, &res) == 0, let addr = res?.pointee.ai_addr { + var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + if getnameinfo(addr, socklen_t(addr.pointee.sa_len), + &hostBuffer, socklen_t(hostBuffer.count), + nil, 0, NI_NAMEREQD) == 0 { + return String(cString: hostBuffer) + } + } + return nil + } + + // Called for each new flow. override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict { os_log("[SC] 🔍] FilterDataProvider: handleNewFlow invoked", log: OSLog.default, type: .debug) // if let appID = flow.sourceAppIdentifier { // print("App making the request: \(appID)") // } +// let appIdAndName = SourceAppAuditTokenBuilder.getAppInfo(from: flow) +// os_log("[SC] 🔍] FilterDataProvider: appIdAndName %@, %@", log: OSLog.default, type: .debug, appIdAndName.appName ?? "", appIdAndName.bundleID ?? "") + guard IPCConnection.shared.blockedUrls.count > 0 else { //No signinficant urls to block + return .allow() + } + + let process = processFromflow(flow: flow) +// os_log(" FilterDataProvider: appIdAndName %{public}@, %{public}@", log: OSLog.default, type: .debug, process?.name ?? "", process?.path ?? "") + os_log(" FilterDataProvider: app %{public}@", log: OSLog.default, type: .debug, process?.description ?? "") + + if let process = process, AllowedProcess.isAllowedProcess(process) { + os_log("[SC] 🔍] FilterDataProvider: allowing as it is in allowed list.") + return .allow() + } guard let socketFlow = flow as? NEFilterSocketFlow else { os_log("[SC] 🔍] Not a socket flow. Allowing.", log: OSLog.default, type: .info) return .allow() } + if socketFlow.direction != .outbound { + os_log("[SC] 🔍] Not a inbound socket flow. Allowing.", log: OSLog.default, type: .info) + return .allow() + } + var remoteHost = "" + if let flowURL = flow.url { + remoteHost = flowURL.absoluteString + os_log("[SC] 🔍] flow.url: \(remoteHost)") + } else { + if let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint { + remoteHost = remoteEndpoint.hostname + let port = remoteEndpoint.port + os_log("[SC] 🔍] NWEndpoint to host: %{public}@, URL: %{public}@, url: %{public}@", log: OSLog.default, type: .debug, remoteHost, port, flow.url?.host() ?? "") + } + } + + + //todo: check host name, work on reverse rule if domain match and if path is nil or blocked, drop connection. +// +// if IPCConnection.shared.blockedList.isMatch(flow as! NEFilterSocketFlow) { +// os_log("[SC] 🔍] Dropped", log: OSLog.default, type: .info) +// return .drop() +// } +// os_log("[SC] 🔍] Allowed", log: OSLog.default, type: .info) +// +// return .allow() + os_log("[SC] 🔍] Flow from remote endpoint: %{public}@, URL: %{public}@", log: OSLog.default, type: .debug, socketFlow.remoteEndpoint.debugDescription, flow.url?.description ?? "nil") + // Extract remote endpoint (if available). - guard let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint else { - os_log("[SC] 🔍] No valid remote endpoint. Allowing flow.", log: OSLog.default, type: .error) - return .allow() - } - os_log("[SC] 🔍] Flow from remote endpoint: %{public}@, URL: %{public}@", log: OSLog.default, type: .debug, remoteEndpoint.description, flow.url?.description ?? "nil") - if let urlString = flow.url?.absoluteString { - os_log("[SC] 🔍] Checking URL path: %{public}@", urlString) -// let blockedHosts = ["google.com/mail", "google.com/news", "facebook.com"] - let blockedHosts = IPCConnection.shared.blockedUrls - os_log("[SC] 🔍] BlockedList: %{public}@ checking:%{public}@",blockedHosts, urlString) - for host in blockedHosts { - if urlString.contains(host) { - os_log("[SC] 🔍] Blocking flow to handleNewFlow %{public}@", urlString) - return .drop() -// return .allow() - } +// guard let remoteEndpoint = socketFlow.remoteEndpoint as? NWHostEndpoint else { +// os_log("[SC] 🔍] No valid remote endpoint. Allowing flow.", log: OSLog.default, type: .error) +// return .allow() +// } + + if remoteHost.isEmpty { //Unable to get host + return .allow() + } + if remoteHost.isValidIpAddress { + if IPCConnection.shared.blockedIPAddresses.contains(remoteHost) { + os_log("[SC] 🔍] Blocking flow IP match: %{public}@", remoteHost) + return .drop() + } + if let host = ReverseDomainMapper.reverseDNSUsingGetNameInfo(ipAddress: remoteHost) { + os_log("[SC] 🔍] Converted IP: %{public}@ to: %{public}@", remoteHost, host) + remoteHost = host + } else { //unable to convert, simple ignore + return .allow() + } + } else { + return .allow() + } + guard let hostDomain = TLDURLToDomain.getURLDomain(from: remoteHost) else { + return .allow() + } + for url in IPCConnection.shared.blockedUrls { + if url.contains(hostDomain) { + os_log("[SC] 🔍] Blocking flow Host match %{public}@, %{public}@", remoteHost, hostDomain) + return .drop() } } +// if var urlString = flow.url?.absoluteString { +// os_log("[SC] 🔍] Checking URL path: %{public}@", urlString) +//// let blockedHosts = ["google.com/mail", "google.com/news", "facebook.com"] +// let blockedHosts = IPCConnection.shared.blockedUrls +// os_log("[SC] 🔍] BlockedList: %{public}@ checking:%{public}@",blockedHosts, urlString) +// if urlString.hasPrefix("www.") { +// let noWWW = String(urlString.dropFirst(4)) +// urlString = noWWW +// } +// +// for host in blockedHosts { +// if urlString.contains(host) { +// os_log("[SC] 🔍] Blocking flow to handleNewFlow %{public}@", urlString) +// return .drop() +//// return .allow() +// } +//// flow.url?.host() == url +// } +// } // if blockedHosts.contains(flow.url?.path() ?? "") { // os_log("Blocking flow to %@", remoteEndpoint.hostname) // return .drop() // } // Only process outbound traffic. - if socketFlow.direction != .outbound { - os_log("[SC] 🔍] Non-outbound traffic. Allowing.", log: OSLog.default, type: .info) - return .allow() - } +// if socketFlow.direction != .outbound { +// os_log("[SC] 🔍] Non-outbound traffic. Allowing.", log: OSLog.default, type: .info) +// return .allow() +// } return .allow() @@ -274,6 +368,18 @@ class FilterDataProvider: NEFilterDataProvider { private func alertUser(for flow: NEFilterSocketFlow) { os_log("[SC] 🔍] Alert: User decision needed for flow %@", log: OSLog.default, type: .info, flow.debugDescription) } + + func processFromflow(flow: NEFilterFlow) -> Process? { + guard let auditTokenData = flow.sourceAppAuditToken else { + return nil + } + + // Convert NSData → audit_token_t + var auditToken = auditTokenData.withUnsafeBytes { ptr -> audit_token_t in + return ptr.load(as: audit_token_t.self) + } + return Process.init(&auditToken) + } } //https://developer.chrome.com/docs/extensions/how-to/distribute/install-extensions diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift new file mode 100644 index 0000000..8857b4f --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/HostToIpMapping.swift @@ -0,0 +1,82 @@ +// +// HostToIpMapping.swift +// SelfControl +// +// Created by Satendra Singh on 20/10/25. +// + +import Foundation + +class HostToIpMapping { + static func string(for addressData: Data) throws -> String { + var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + + let result = addressData.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> Int32 in + guard let addrPtr = ptr.baseAddress else { + return EINVAL + } + return getnameinfo( + addrPtr.assumingMemoryBound(to: sockaddr.self), + socklen_t(addressData.count), + &hostBuffer, + socklen_t(hostBuffer.count), + nil, + 0, + NI_NUMERICHOST + ) + } + + if result != 0 { + let message = String(cString: gai_strerror(result)) + throw NSError(domain: "HostToIpMappingError", code: Int(result), userInfo: [ + NSLocalizedDescriptionKey: message + ]) + } + + return String(cString: hostBuffer) + } + + static func ipAddresses(for domainName: String) -> [String] { + let startTime = Date() + + // ✅ Correct initialization + let cfHostOpt: CFHost? = CFHostCreateWithName(kCFAllocatorDefault, domainName as CFString).takeRetainedValue() + guard let cfHost = cfHostOpt else { + print("HostToIpMapping: Failed to create CFHost for \(domainName)") + return [] + } + + var streamError = CFStreamError() + let success = CFHostStartInfoResolution(cfHost, .addresses, &streamError) + + if !success || streamError.error != 0 { + print("HostToIpMapping: Warning: failed to resolve addresses for \(domainName) with stream error \(streamError.error)") + return [] + } + + // ✅ Use takeUnretainedValue safely + guard let addressArray = CFHostGetAddressing(cfHost, nil)? + .takeUnretainedValue() as? [Data] else { + print("HostToIpMapping: Warning: failed to resolve addresses for \(domainName)") + return [] + } + + var stringAddresses: [String] = [] + + for addrData in addressArray { + do { + let ip = try string(for: addrData) + stringAddresses.append(ip) + } catch { + print("HostToIpMapping: Warning: Failed to parse IP struct for \(domainName) with error: \(error.localizedDescription)") + } + } + + let elapsed = Date().timeIntervalSince(startTime) + if elapsed > 2.5 { + print("HostToIpMapping: Warning: took \(elapsed) seconds to resolve \(domainName)") + } + + return stringAddresses + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift index 79ebcc5..286fbf6 100644 --- a/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/IPCConnection.swift @@ -13,6 +13,7 @@ import Network @objc protocol ProviderCommunication { func register(_ completionHandler: @escaping (Bool) -> Void) func setBlockedURLs(_ urls: [String]) + func setBlockedIPAddresses(_ ips: [String]) } /// Provider --> App IPC @@ -38,6 +39,8 @@ class IPCConnection: NSObject { static let shared = IPCConnection() // var blockedUrls: [String] = ProxyPreferences.getBlockedDomains() var blockedUrls: [String] = [String]() + var blockedList = BlockOrAllowList(items: []) + var blockedIPAddresses: Set = [] // MARK: Methods @@ -164,14 +167,32 @@ extension IPCConnection: NSXPCListenerDelegate { } providerProxy.setBlockedURLs(urls) } + + func enableIPAddressesBlocking(_ urls: [String]) { + os_log("[SC] 🔍] Enabling URL blocking") + guard let providerProxy = currentConnection?.remoteObjectProxyWithErrorHandler({ registerError in + os_log("[SC] 🔍] Failed to register with the provider: %@", registerError.localizedDescription) + }) as? ProviderCommunication else { + fatalError("Failed to create a remote object proxy for the provider") + } + providerProxy.setBlockedIPAddresses(urls) + } } + extension IPCConnection: ProviderCommunication { + + func setBlockedIPAddresses(_ ips: [String]) { + blockedIPAddresses = Set(ips) + os_log("[SC] 🔍] setBlockedIPAddresses: %{public}@", blockedIPAddresses) + } + func setBlockedURLs(_ urls: [String]) { os_log("[SC] 🔍] Blocking: %{public}@",urls) blockedUrls = urls // delegate?.didSetUrls() - + blockedList = BlockOrAllowList(items: blockedUrls) + guard let connection = currentConnection else { print("[SC] 🔍] Cannot update blocked urls, app isn't registered") return diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift new file mode 100644 index 0000000..3397339 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Binary.swift @@ -0,0 +1,175 @@ +// +// Binary.swift +// SelfControl +// +// Created by Satendra Singh on 10/10/25. +// + +import Foundation +import AppKit +import OSLog +import Security +import CoreServices // for MDItem + +@objcMembers +class Binary: NSObject { + + // MARK: - Properties (mirroring Binary.h) + + dynamic var path: String + dynamic var name: String = "" + dynamic var icon: NSImage = NSImage() + dynamic var attributes: NSDictionary? + dynamic var metadata: NSDictionary? + dynamic var bundle: Bundle? + dynamic var csInfo: NSMutableDictionary = NSMutableDictionary() + dynamic var sha256: NSMutableString = NSMutableString() + + // MARK: - Init + + init(_ path: String) { + self.path = (path as NSString).resolvingSymlinksInPath + super.init() + + // Try load app bundle (nil for non-apps) + self.getBundle() + + // Get name + self.getName() + + // File attributes + self.getAttributes() + + // Spotlight metadata + self.getMetadata() + } + + // MARK: - Bundle + + // Try load app bundle; nil for non-apps + func getBundle() { + // First try direct path + if let b = Bundle(path: self.path) { + self.bundle = b + return + } + // Else find dynamically + self.bundle = Utilities().findAppBundle(self.path) + } + + // MARK: - Name + + // Figure out binary's name via bundle CFBundleName or lastPathComponent + func getName() { + if let bundle = self.bundle, + let bundleName = bundle.infoDictionary?["CFBundleName"] as? String { + self.name = bundleName + } else { + self.name = (self.path as NSString).lastPathComponent + } + } + + // MARK: - Attributes + + func getAttributes() { + self.attributes = try? FileManager.default.attributesOfItem(atPath: self.path) as NSDictionary + } + + // MARK: - Spotlight metadata + + func getMetadata() { + // MDItemCreate wants CFString path + let cfPath = self.path as CFString + guard let mdItem = MDItemCreate(kCFAllocatorDefault, cfPath) else { + return + } + defer { } + + guard let attributeNames = MDItemCopyAttributeNames(mdItem) else { + return + } + defer { } + + if let attrs = MDItemCopyAttributes(mdItem, attributeNames) { + self.metadata = attrs as NSDictionary + } + } + + // MARK: - Icon + + // Get an icon for the process: app’s icon or system one + func getIcon() { + // Skip short/non-absolute paths if no bundle (system logs errors otherwise) + if !self.path.hasPrefix("/"), self.bundle == nil { + return + } + + // For apps, try CFBundleIconFile + if let bundle = self.bundle { + if let iconFile = bundle.infoDictionary?["CFBundleIconFile"] as? String { + let iconExt = (iconFile as NSString).pathExtension.isEmpty ? "icns" : (iconFile as NSString).pathExtension + let iconBase = (iconFile as NSString).deletingPathExtension + if let iconPath = bundle.path(forResource: iconBase, ofType: iconExt) { + if let img = NSImage(contentsOfFile: iconPath) { + self.icon = img + } + } + } + } + + // Fallback to workspace icon + if self.bundle == nil || self.icon.size == .zero { + self.icon = NSWorkspace.shared.icon(forFile: self.path) + } + + // Standard size 128x128 + self.icon.size = NSSize(width: 128, height: 128) + } + + // MARK: - Code signing (static) + + // You likely have this helper already, but defining its expected signature for clarity: + func extractSigningInfo(_ code: SecStaticCode?, _ path: String, _ flags: SecCSFlags) -> [String: Any]? { + var staticCode: SecStaticCode? + let url = URL(fileURLWithPath: path) as CFURL + let status = SecStaticCodeCreateWithPath(url, SecCSFlags(), &staticCode) + guard status == errSecSuccess, let codeRef = staticCode else { + return nil + } + + var signingInfo: CFDictionary? + let infoStatus = SecCodeCopySigningInformation(codeRef, flags, &signingInfo) + guard infoStatus == errSecSuccess, let info = signingInfo as? [String: Any] else { + return nil + } + return info + } + + // Your corrected function: + func generateSigningInfo(_ flags: SecCSFlags) { + // These constants are C macros in Security framework headers, not imported automatically: + let kSecCodeInfoStatus = "Status" // corresponds to kSecCodeInfoStatus in Security framework + let KEY_CS_STATUS = kSecCodeInfoStatus // for compatibility with your existing code + + // Instead of 'nil', explicitly pass an optional SecStaticCode? = nil + if let extracted = extractSigningInfo(nil as SecStaticCode?, self.path, flags), + let statusNum = extracted[KEY_CS_STATUS] as? NSNumber, + statusNum.intValue == Int(noErr) { + // Bridge [String: Any] to NSMutableDictionary + self.csInfo = NSMutableDictionary(dictionary: extracted) + } else { + // logging commented out in original code + } + } + + // MARK: - Description + + override var description: String { + return String(format: "name: %@\npath: %@\nattributes: %@\nsigning info: %@\nmetadata: %@", + self.name, + self.path, + self.attributes ?? [:], + self.csInfo, + self.metadata ?? [:]) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift new file mode 100644 index 0000000..823bb5b --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Process.swift @@ -0,0 +1,330 @@ +// +// Process.swift +// SelfControl +// +// Created by Satendra Singh on 10/10/25. +// + +import Foundation +import OSLog +import Security +import Darwin +import NetworkExtension + +@objcMembers +class Process: NSObject { + + // MARK: - Properties (mirroring Process.h) + + dynamic var pid: pid_t = -1 + dynamic var uid: uid_t = UInt32(bitPattern: -1) + dynamic var type: UInt16 = 0 + dynamic var exit: UInt32 = UInt32(bitPattern: -1) + dynamic var deleted: Bool = false + + dynamic var name: String? + dynamic var path: String? + dynamic var arguments: NSMutableArray? = NSMutableArray() + dynamic var ancestors: NSMutableArray? = NSMutableArray() + dynamic var csInfo: NSMutableDictionary? + dynamic var key: String = "" + dynamic var bundleID: String = "" + dynamic var binary: Binary = Binary.init("") + dynamic var timestamp: Date = Date() + + // MARK: - Init + + override init() { + super.init() + self.arguments = NSMutableArray() + self.ancestors = NSMutableArray() + self.timestamp = Date() + self.pid = -1 + self.uid = UInt32(bitPattern: -1) + self.exit = UInt32(bitPattern: -1) + } + + // Init with audit token pointer + // Matches -(id)init:(audit_token_t*)token + convenience init?(_ token: UnsafePointer) { + self.init() + + // Save pid + let tokenVal = token.pointee + let procPID = audit_token_to_pid(tokenVal) + self.pid = procPID + if self.pid == 0 { +// os_log_error(logHandle, "ERROR: 'audit_token_to_pid' returned NULL") + return nil + } + + // Get path via SecCode (also sets deleted flag) + self.getPath(token) + if (self.path ?? "").isEmpty { +// os_log_error(logHandle, "ERROR: failed to find path for process %d", self.pid) + return nil + } + + // Set name + if self.deleted != true { + self.name = Utilities().getProcessName(0, self.path!) + } else { + self.name = Utilities().getProcessName(self.pid, self.path!) + } + + // Get user + self.uid = audit_token_to_euid(tokenVal) + self.bundleID = Utilities().getBundleID(self.path ?? "") ?? "" + // Generate (dynamic) code information + self.generateSigningInfo(token) + + // Generate key + self.key = self.generateKey() + + // Init binary + self.binary = Binary(self.path ?? "") + + // PID specific logic: args and ancestors + self.getArgs() + self.ancestors = Utilities().generateProcessHierarchy(self.pid) + + // Verify pid-version (avoid pid reuse) + if let currentTokenNSData = Utilities().tokenForPid(self.pid), + currentTokenNSData.length == MemoryLayout.size { + let currentTokenData = Data(referencing: currentTokenNSData) + currentTokenData.withUnsafeBytes { (rawPtr: UnsafeRawBufferPointer) in + if let curPtr = rawPtr.baseAddress?.assumingMemoryBound(to: audit_token_t.self) { + let origVersion = audit_token_to_pidversion(tokenVal) + let curVersion = audit_token_to_pidversion(curPtr.pointee) + if origVersion != curVersion { +// os_log_error(logHandle, "ERROR: audit token mismatch ...pid re-used?") + self.arguments = nil + self.ancestors = nil + } + } + } + } + + // Process alive check + if Utilities().isAlive(self.pid) != true { +// os_log_error(logHandle, "ERROR: process (%d)%{public}@ has already exited", self.pid, self.path ?? "") + return nil + } + } + + // MARK: - Key generation + + // Matches -(NSString*)generateKey + func generateKey() -> String { + var id: String = "" + var signer: Int = 0 + enum Signer: Int { + case None = 0 + case Apple + case AppStore + case DevID + case AdHoc + } + + if let cs = self.csInfo { + if let v = cs[Constants.KEY_CS_SIGNER] as? NSNumber { + signer = v.intValue + } + + // Apple/App Store: just use cs id + if signer == Signer.Apple.rawValue || signer == Signer.AppStore.rawValue { + if let csid = cs[Constants.KEY_CS_ID] as? String, !csid.isEmpty { + id = csid + } + } + // Dev ID: use cs id + leaf signer + else if signer == Int(Signer.DevID.rawValue) { + if let csid = cs[Constants.KEY_CS_ID] as? String, !csid.isEmpty, + let auths = cs[Constants.KEY_CS_AUTHS] as? [Any], let first = auths.first as? String, !first.isEmpty { + id = "\(csid):\(first)" + } + } + } + + if id.isEmpty { + id = self.path ?? "" + } + +// os_log_debug(logHandle, "generated process key: %{public}@", id) + return id + } + + // MARK: - Path + + // Matches -(void)getPath:(audit_token_t*)token + func getPath(_ token: UnsafePointer) { + var status: OSStatus = errSecParam + var code: SecCode? + var staticCode: SecStaticCode? + var pathURL: CFURL? + + // SecCodeCopyGuestWithAttributes using audit token + let attrs: [String: Any] = [kSecGuestAttributeAudit as String: Data(bytes: token, count: MemoryLayout.size)] + status = SecCodeCopyGuestWithAttributes(nil, attrs as CFDictionary, SecCSFlags(), &code) + if status == errSecSuccess, let code = code { + // Convert dynamic code (SecCode) to static code (SecStaticCode) + status = SecCodeCopyStaticCode(code, SecCSFlags(), &staticCode) + if status == errSecSuccess, let staticCode = staticCode { + status = SecCodeCopyPath(staticCode, SecCSFlags(), &pathURL) + if status == errSecSuccess, let url = pathURL as NSURL? { + self.path = (url as URL).path + } else { +// os_log_error(logHandle, "ERROR: 'SecCodeCopyPath' failed with': %#x", status) + } + } else { +// os_log_error(logHandle, "ERROR: 'SecCodeCopyStaticCode' failed with': %#x", status) + } + } else { +// os_log_error(logHandle, "ERROR: 'SecCodeCopyGuestWithAttributes' failed with': %#x", status) + } + + // Deleted binary? + if status == OSStatus(kPOSIXErrorENOENT) { +// os_log_debug(logHandle, "process %d's binary appears to be deleted", self.pid) + self.deleted = true + } + + // Fallback + if pathURL == nil { + self.path = Utilities().getProcessPath(self.pid) + } + + if let p = self.path { + self.path = (p as NSString).resolvingSymlinksInPath + } + + // No CFRelease in Swift ARC; CoreFoundation objects are memory-managed automatically. + } + + // MARK: - Args + + // Matches -(void)getArgs + func getArgs() { + var mib: [Int32] = [CTL_KERN, KERN_ARGMAX, 0] + var systemMaxArgs: Int32 = 0 + var size = MemoryLayout.size(ofValue: systemMaxArgs) + + // Get system arg max + if sysctl(&mib, 2, &systemMaxArgs, &size, nil, 0) == -1 { + return + } + + guard systemMaxArgs > 0 else { return } + let bufSize = Int(systemMaxArgs) + let processArgs = UnsafeMutablePointer.allocate(capacity: bufSize) + defer { processArgs.deallocate() } + + mib = [CTL_KERN, KERN_PROCARGS2, Int32(self.pid)] + size = bufSize + + if sysctl(&mib, 3, processArgs, &size, nil, 0) == -1 { + return + } + + if size <= MemoryLayout.size { + return + } + + // numberOfArgs at start + var numberOfArgs: Int32 = 0 + memcpy(&numberOfArgs, processArgs, MemoryLayout.size) + + // parser after numberOfArgs + var parser = processArgs.advanced(by: MemoryLayout.size) + let endPtr = processArgs.advanced(by: size) + + // Skip executable path (NULL-terminated) + while parser < endPtr { + if parser.pointee == 0 { break } + parser = parser.advanced(by: 1) + } + if parser == endPtr { return } + + // Skip trailing NULLs to argv[0] + while parser < endPtr { + if parser.pointee != 0 { break } + parser = parser.advanced(by: 1) + } + if parser == endPtr { return } + + // Now parse args until count reached + var argStart = parser + let argsArray = NSMutableArray() + while parser < endPtr { + if parser.pointee == 0 { + if let arg = String(validatingUTF8: UnsafePointer(argStart)) { + argsArray.add(arg) + } + parser = parser.advanced(by: 1) + argStart = parser + if argsArray.count == Int(numberOfArgs) { + break + } + continue + } + parser = parser.advanced(by: 1) + } + + self.arguments = argsArray + } + + // MARK: - Code signing + + // Local key mapping for Security's kSecCodeInfoStatus + private let KEY_CS_STATUS = "Status" + + // Helper to extract signing info from an audit token (dynamic code) + private func extractSigningInfo(_ token: UnsafeMutablePointer, + _ requirement: SecRequirement?, + _ flags: SecCSFlags) -> NSMutableDictionary? { + // Build attributes with audit token + let attrs: [String: Any] = [kSecGuestAttributeAudit as String: + Data(bytes: token, count: MemoryLayout.size)] + var code: SecCode? + var status = SecCodeCopyGuestWithAttributes(nil, attrs as CFDictionary, flags, &code) + guard status == errSecSuccess, let codeRef = code else { + return nil + } + + // Convert SecCode (dynamic) to SecStaticCode before querying signing info + var staticCode: SecStaticCode? + status = SecCodeCopyStaticCode(codeRef, flags, &staticCode) + guard status == errSecSuccess, let staticCodeRef = staticCode else { + return nil + } + + var info: CFDictionary? + status = SecCodeCopySigningInformation(staticCodeRef, flags, &info) + guard status == errSecSuccess, let dict = info as? [String: Any] else { + return nil + } + return NSMutableDictionary(dictionary: dict) + } + + // Matches -(void)generateSigningInfo:(audit_token_t*)token + func generateSigningInfo(_ token: UnsafePointer) { + // Pass typed nil for requirement to avoid "nil requires a contextual type" + let requirement: SecRequirement? = nil + if let extracted = extractSigningInfo(UnsafeMutablePointer(mutating: token), requirement, SecCSFlags()) { + if let statusNum = extracted[KEY_CS_STATUS] as? NSNumber, statusNum.intValue == Int(noErr) { + self.csInfo = extracted + } else { +// os_log_error(logHandle, "ERROR: invalid code signing information for %{public}@: %{public}@", self.path ?? "", extracted) + } + } else { +// os_log_error(logHandle, "ERROR: failed to extract code signing information for %{public}@", self.path ?? "") + } + } + + // MARK: - Description + + override var description: String { + return String(format: "pid: %d\npath: %@\nuser: %d\nargs: %@\nancestors: %@\n signing info: %@\n binary:\n%@", self.pid, self.path ?? "nil", self.uid, self.arguments ?? [], self.ancestors ?? [], self.csInfo ?? [:], self.binary.description) + } +} + diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift new file mode 100644 index 0000000..68b9641 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/SourceAppAuditTokenBuilder.swift @@ -0,0 +1,61 @@ +// +// SourceAppAuditTokenBuilder.swift +// SelfControlExtension +// +// Created by Satendra Singh on 10/10/25. +// + +import NetworkExtension +import Security +import Foundation +import AppKit + +struct SourceAppAuditTokenBuilder { + + static func getAppInfo(from flow: NEFilterFlow) -> (bundleID: String?, appName: String?) { + guard let auditTokenData = flow.sourceAppAuditToken else { + return (nil, nil) + } + + // Convert NSData → audit_token_t + let auditToken = auditTokenData.withUnsafeBytes { ptr -> audit_token_t in + return ptr.load(as: audit_token_t.self) + } + + // MARK: 1. Extract PID from audit token + let pid = audit_token_to_pid(auditToken) + + // MARK: 2. Use SecTask to get bundle identifier + var bundleID: String? = nil + if let secTask = SecTaskCreateWithAuditToken(nil, auditToken) { + // macOS doesn’t define kSecEntitlementApplicationIdentifier constant, + // so use the raw entitlement string instead: + let entitlementKey = "application-identifier" as CFString + bundleID = SecTaskCopyValueForEntitlement(secTask, entitlementKey, nil) as? String + } + + // MARK: 3. Try to get app name from bundle ID + var appName: String? = nil + if let bundleID = bundleID, + let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) { + appName = appURL.deletingPathExtension().lastPathComponent + } + + // MARK: 4. Fallback: get executable name via proc_pidpath() + if appName == nil { + var pathBuffer = [CChar](repeating: 0, count: Int(PATH_MAX)) + let result = proc_pidpath(pid, &pathBuffer, UInt32(pathBuffer.count)) + if result > 0 { + let path = String(cString: pathBuffer) + appName = URL(fileURLWithPath: path).lastPathComponent + } + } + + return (bundleID, appName) + } + + // Helper: extract PID from audit_token_t + private static func audit_token_to_pid(_ token: audit_token_t) -> pid_t { + return pid_t(token.val.0) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift new file mode 100644 index 0000000..0ee8216 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/Process/Utilities.swift @@ -0,0 +1,714 @@ +// +// File.swift +// SelfControl +// +// Created by Satendra Singh on 10/10/25. +// + + +// Swift port of utilities.m +// +// Note: This file relies on C system APIs. Ensure your bridging header +// imports the needed headers: libproc.h, sys/sysctl.h, CommonCrypto/CommonCrypto.h, +// mach/mach.h, Security/Security.h, Carbon/Carbon.h, CoreServices/CoreServices.h, CFNetwork/CFHost.h +// + +import AppKit +import Foundation +import OSLog +import SystemConfiguration +import Darwin +import CoreServices // for MDItem +import Security +import os.log +import os + +// Define C macro from libproc.h (not imported automatically into Swift) +private let PROC_PIDPATHINFO_MAXSIZE: Int = 4096 + +// MARK: - Globals + +final class Utilities { + // Provided elsewhere in project + + let logger = Logger(subsystem: "com.example.myapp", category: "network") + + // MARK: - Version / Bundle + + @objc + public func getAppVersion() -> String? { + return Bundle.main.infoDictionary?["CFBundleVersion"] as? String + } + + @objc + public func getBundleExecutable(_ appPath: String) -> String? { + guard let bundle = Bundle(path: appPath) else { + logger.error("ERROR: failed to load app bundle for %{public}\(appPath)") + return nil + } + return (bundle.executablePath as NSString?)?.resolvingSymlinksInPath + } + + // MARK: - Parent process (Carbon/deprecated) + + @objc + public func getRealParent(_ pid: pid_t) -> NSDictionary? { + let PROC_PIDPATHINFO_MAXSIZE = 4096 + + // Step 1: Get parent PID + var info = kinfo_proc() + var size = MemoryLayout.stride + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid] + + guard sysctl(&mib, u_int(mib.count), &info, &size, nil, 0) == 0 else { + return nil + } + + let ppid = info.kp_eproc.e_ppid + if ppid == 0 { return nil } + + // Step 2: Get parent process path + var pathBuffer = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE)) + let result = proc_pidpath(ppid, &pathBuffer, UInt32(pathBuffer.count)) + let path = (result > 0) ? String(cString: pathBuffer) : nil + + // Step 3: Try to get bundle info via NSRunningApplication + var appName: String? = nil + var bundleID: String? = nil + + if let app = NSRunningApplication(processIdentifier: ppid) { + appName = app.localizedName + bundleID = app.bundleIdentifier + } + + // Step 4: Construct NSDictionary for compatibility + let dict: NSDictionary = [ + "ParentPID": NSNumber(value: ppid), + "ParentPath": path ?? "", + "ParentAppName": appName ?? "", + "ParentBundleID": bundleID ?? "" + ] + + return dict + } + + + // MARK: - Process hierarchy + + @objc + public func generateProcessHierarchy(_ child: pid_t) -> NSMutableArray { + let ancestors = NSMutableArray() + typealias RPIDFunc = @convention(c) (pid_t) -> pid_t + let rpidSym = dlsym(dlopen(nil, RTLD_NOW), "responsibility_get_pid_responsible_for_pid") + let getRPID = rpidSym.map { unsafeBitCast($0, to: RPIDFunc.self) } + + var currentPID = child + + while true { + let currentPath = getProcessPath(currentPID) ?? NSLocalizedString("unknown", comment: "unknown") + let currentName = getProcessName(0, currentPath) ?? NSLocalizedString("unknown", comment: "unknown") + ancestors.insert([ + Constants.KEY_PROCESS_ID: NSNumber(value: currentPID), + Constants.KEY_PROCESS_PATH: currentPath, + Constants.KEY_PROCESS_NAME: currentName + ], at: 0) + + var parentPID: pid_t = 0 + + if getuid() != 0 { + if let parent = getRealParent(currentPID), let p = parent["pid"] as? NSNumber { + parentPID = pid_t(truncating: p) + } + } + + if parentPID == 0, let getRPID = getRPID { + parentPID = getRPID(currentPID) + } + + if parentPID <= 0 || parentPID == currentPID { + parentPID = getParent(Int32(currentPID)) + } + + if parentPID <= 0 || parentPID == currentPID { + break + } + + currentPID = parentPID + } + + // add KEY_INDEX for UI + for i in 0.. String? { + var uid: uid_t = 0 + var gid: gid_t = 0 + + // explicitly type `nil` as `SCDynamicStore?` + if let cfUser = SCDynamicStoreCopyConsoleUser(nil as SCDynamicStore?, &uid, &gid) { + return cfUser as String + } + return nil + } + + // MARK: - Process name + + @objc + public func getProcessName(_ pid: pid_t, _ path: String) -> String? { + + let PROC_PIDPATHINFO_MAXSIZE = 4096 // actual constant value from libproc.h + + if pid != 0 { + var nameBuf = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE)) + let status = proc_name(pid, &nameBuf, UInt32(nameBuf.count)) + if status >= 0 { + return String(cString: nameBuf) + } + } + + if let bundle = findAppBundle(path), + let name = bundle.infoDictionary?["CFBundleName"] as? String { + return name + } + + return (path as NSString).lastPathComponent + } + + @objc + public func getBundleID(_ path: String) -> String? { + if let bundle = findAppBundle(path), + let name = bundle.infoDictionary?["CFBundleIdentifier"] as? String { + return name + } + return nil + } + + // MARK: - Find app bundle + + @objc + public func findAppBundle(_ path: String) -> Bundle? { + let standardized = ((path as NSString).standardizingPath as NSString).resolvingSymlinksInPath + var appPath: NSString = standardized as NSString + + while true { + if let bundle = Bundle(path: appPath as String) { + if bundle.bundlePath == standardized { return bundle } + if bundle.executablePath == standardized { return bundle } + } + + let next = appPath.deletingLastPathComponent as NSString + if next.length == 0 || next as String == "/" { break } + appPath = next + } + + return nil + } + + // MARK: - Process path + + @objc + public func getProcessPath(_ pid: pid_t) -> String? { + var buf = [CChar](repeating: 0, count: Int(PROC_PIDPATHINFO_MAXSIZE)) + let status = proc_pidpath(pid, &buf, UInt32(buf.count)) + if status > 0 { + return String(cString: buf) + } else { +// os_log_error(logHandle, "ERROR: for process %d, 'proc_pidpath' failed with %d (errno: %d)", pid, status, errno) + + var mib = [CTL_KERN, KERN_ARGMAX, 0] + var argMax: Int = 0 + var size = MemoryLayout.size(ofValue: argMax) + if sysctl(&mib, 2, &argMax, &size, nil, 0) == -1 { return nil } + + let argBuf = UnsafeMutablePointer.allocate(capacity: argMax) + defer { argBuf.deallocate() } + + mib = [CTL_KERN, KERN_PROCARGS2, Int32(pid)] + size = argMax + if sysctl(&mib, 3, argBuf, &size, nil, 0) == -1 { return nil } + if size <= MemoryLayout.size { return nil } + + // argv0 path follows int argc + let pathPtr = argBuf.advanced(by: MemoryLayout.size) + let p = String(cString: pathPtr) + + if p.hasPrefix("./") { + let trimmed = String(p.dropFirst(2)) + if let cwd = getProcessCWD(pid) { + return (cwd as NSString).appendingPathComponent(trimmed) + } + } + + return p + } + } + + // MARK: - CWD + + @objc + public func getProcessCWD(_ pid: pid_t) -> String? { + var vpi = proc_vnodepathinfo() + let status = withUnsafeMutablePointer(to: &vpi) { ptr -> Int32 in + return proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, ptr, Int32(MemoryLayout.size)) + } + if status > 0 { + return withUnsafePointer(to: vpi.pvi_cdir.vip_path) { ptr in + return String(cString: UnsafeRawPointer(ptr).assumingMemoryBound(to: CChar.self)) + } + } + return nil + } + + // MARK: - PIDs for path/user + + @objc + public func getProcessIDs(_ processPath: String, _ userID: Int32) -> NSMutableArray { + let result = NSMutableArray() + + let count = proc_listallpids(nil, 0) + guard count > 0 else { return result } + + let pids = UnsafeMutablePointer.allocate(capacity: Int(count)) + defer { pids.deallocate() } + + let status = proc_listallpids(pids, count * Int32(MemoryLayout.size)) + guard status >= 0 else { return result } + + var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, 0] + var kproc = kinfo_proc() + let kprocSize = MemoryLayout.size + + for i in 0.. NSImage? { + // invalid path -> generic application icon + guard FileManager.default.fileExists(atPath: path) else { + let icon = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericApplicationIcon))) + icon.size = NSSize(width: 128, height: 128) + return icon + } + + if let bundle = findAppBundle(path) { + if let icon = NSWorkspace.shared.icon(forFile: bundle.bundlePath) as NSImage? { + return icon + } + + if let iconFile = bundle.infoDictionary?["CFBundleIconFile"] as? NSString { + var ext = iconFile.pathExtension + if ext.isEmpty { ext = "icns" } + if let iconPath = bundle.path(forResource: iconFile.deletingPathExtension, ofType: ext) { + return NSImage(contentsOfFile: iconPath) + } + } + } + + var icon = NSWorkspace.shared.icon(forFile: path) + // replace generic document icon with application icon + let documentIcon = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericDocumentIcon))) + if icon == documentIcon { + icon = NSWorkspace.shared.icon(forFileType: NSFileTypeForHFSTypeCode(OSType(kGenericApplicationIcon))) + } + icon.size = NSSize(width: 128, height: 128) + return icon + } + + // MARK: - Make modal + + @objc + public func makeModal(_ controller: NSWindowController) { + var window: NSWindow? + + for _ in 0..<20 { + DispatchQueue.main.sync { + window = controller.window + } + if window == nil { + Thread.sleep(forTimeInterval: 0.05) + continue + } + DispatchQueue.main.sync { + NSApplication.shared.runModal(for: controller.window!) + } + break + } + } + + // MARK: - Find processes by name + + @objc + public func findProcesses(_ processName: String) -> NSMutableArray { + let processes = NSMutableArray() + let count = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0) + guard count > 0 else { return processes } + + let pids = UnsafeMutablePointer.allocate(capacity: Int(count)) + defer { pids.deallocate() } + + let status = proc_listpids(UInt32(PROC_ALL_PIDS), 0, pids, count * Int32(MemoryLayout.size)) + guard status >= 0 else { return processes } + + for i in 0.. Date? { +// os_log_debug(logHandle, "extracting 'kMDItemDateAdded' for %{public}@", file) +// +// let url: URL +// if let bundle = findAppBundle(file) { +// url = bundle.bundleURL +// } else { +// url = URL(fileURLWithPath: file) +// } +// +// guard let item = MDItemCreateWithURL(nil, url as CFURL) else { return nil } +// defer { CFRelease(item) } +// +// if let date = MDItemCopyAttribute(item, kMDItemDateAdded)?.takeRetainedValue() as? Date { +// os_log_debug(logHandle, "extacted date, %{public}@, for %{public}@", String(describing: date), file) +// return date +// } else { +// os_log_debug(logHandle, "'kMDItemDateAdded' is nil ...falling back to 'kMDItemFSCreationDate'") +// if let date = MDItemCopyAttribute(item, kMDItemFSCreationDate)?.takeRetainedValue() as? Date { +// os_log_debug(logHandle, "extacted date, %{public}@, for %{public}@", String(describing: date), file) +// return date +// } +// } +// return nil +// } + + // MARK: - Parent PID + + @objc + public func getParent(_ pid: Int32) -> pid_t { + var kproc = kinfo_proc() + var size = MemoryLayout.size + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid] // 👈 consistent Int32s + let res = sysctl(&mib, u_int(mib.count), &kproc, &size, nil, 0) + if res == 0 && size != 0 { + let ppid = kproc.kp_eproc.e_ppid +// os_log_debug(logHandle, "extracted parent ID %d for process: %d", ppid, pid) + return ppid + } + return -1 + } + + // MARK: - Dark mode + + @objc + public func isDarkMode() -> Bool { + return UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" + } + + // MARK: - String default + + @objc + public func valueForStringItem(_ item: String?) -> String { + return item ?? NSLocalizedString("unknown", comment: "unknown") + } + + // MARK: - Alerts + + @objc + public func showAlert(_ style: NSAlert.Style, _ messageText: String, _ informativeText: String?, _ buttons: [String]) -> NSApplication.ModalResponse { + let alert = NSAlert() + alert.alertStyle = style + alert.messageText = messageText + if let info = informativeText { + alert.informativeText = info + } + for title in buttons { + alert.addButton(withTitle: title) + } + if let first = alert.buttons.first { + first.keyEquivalent = "\r" + } + + NSApp.setActivationPolicy(.regular) + if #available(macOS 14.0, *) { + NSApp.activate() + } else { + NSApp.activate(ignoringOtherApps: true) + } + NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) + + alert.window.makeKeyAndOrderFront(nil) + alert.window.center() + + let response = alert.runModal() +// (NSApp.delegate as? AppDelegate)?.setActivationPolicy() + return response + } + + // MARK: - Audit token for pid + + // Swift doesn't expose this C macro — we define it manually. + let TASK_AUDIT_TOKEN_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + + @objc + public func tokenForPid(_ pid: pid_t) -> NSData? { + var task: mach_port_t = 0 + var token = audit_token_t() + var size = TASK_AUDIT_TOKEN_COUNT + +// os_log_debug(logHandle, "retrieving audit token for %d", pid) + + var kr = task_name_for_pid(mach_task_self_, pid, &task) + guard kr == KERN_SUCCESS else { +// os_log_error(logHandle, "ERROR: 'task_name_for_pid' failed with %x", kr) + return nil + } + defer { mach_port_deallocate(mach_task_self_, task) } + + kr = withUnsafeMutablePointer(to: &token) { ptr in + ptr.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { intPtr in + return task_info(task, task_flavor_t(TASK_AUDIT_TOKEN), intPtr, &size) + } + } + + guard kr == KERN_SUCCESS else { +// os_log_error(logHandle, "ERROR: 'task_info' failed with %x", kr) + return nil + } + +// os_log_debug(logHandle, "retrieved audit token") + return NSData(bytes: &token, length: MemoryLayout.size) + } + + // MARK: - Reverse DNS resolve + + @objc + public func resolveAddress(_ ipAddr: String) -> NSArray? { +// os_log_debug(logHandle, "(attempting to) reverse resolve %{public}@", ipAddr) + + var hints = addrinfo(ai_flags: AI_NUMERICHOST, ai_family: PF_UNSPEC, ai_socktype: SOCK_STREAM, ai_protocol: 0, ai_addrlen: 0, ai_canonname: nil, ai_addr: nil, ai_next: nil) + var res: UnsafeMutablePointer? + guard getaddrinfo(ipAddr, nil, &hints, &res) == 0, let result = res else { + return nil + } + defer { freeaddrinfo(result) } + + guard let addrData = CFDataCreate(nil, UnsafePointer(OpaquePointer(result.pointee.ai_addr)), CFIndex(result.pointee.ai_addrlen)) else { + return nil + } + defer { /*addrData*/ } + + let host = CFHostCreateWithAddress(kCFAllocatorDefault, addrData).takeRetainedValue() + var streamErr = CFStreamError() + guard CFHostStartInfoResolution(host, .names, &streamErr) else { + return nil + } + guard let names = CFHostGetNames(host, nil)?.takeUnretainedValue() as NSArray? else { + return nil + } + return names + } + + // MARK: - Process alive + + @objc + public func isAlive(_ processID: pid_t) -> Bool { + errno = 0 + _ = kill(processID, 0) + if errno == ESRCH { return false } + + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, Int32(processID)] + var info = kinfo_proc() + var size = MemoryLayout.size + if sysctl(&mib, 4, &info, &size, nil, 0) == 0 { + // SZOMB check + if (UInt8(info.kp_proc.p_stat) & UInt8(SZOMB)) == UInt8(SZOMB) { + return false + } + } + return true + } + + // MARK: - Simulator app? + + @objc + public func isSimulatorApp(_ path: String) -> Bool { +// os_log_debug(logHandle, "checking if %{public}@ is a simulator application", path) + guard let bundle = findAppBundle(path) else { return false } + guard let platforms = bundle.infoDictionary?["CFBundleSupportedPlatforms"] as? [String], !platforms.isEmpty else { + return false + } +// os_log_debug(logHandle, "supported platforms: %{public}@", String(describing: platforms)) + let set = Set(platforms) + return set.isSubset(of: Set(["iPhoneSimulator", "AppleTVSimulator"])) + } + + // MARK: - Launched by user? + + @objc + public func launchedByUser() -> Bool { + guard let parent = getRealParent(getpid()) else { return false } + let bid = parent["CFBundleIdentifier"] as? NSString + if bid == "com.apple.dock" || bid == "com.apple.finder" || bid == "com.apple.Terminal" { + return true + } + return false + } + + // MARK: - Fade out + + @objc + public func fadeOut(_ window: NSWindow, _ duration: Float) { + NSAnimationContext.runAnimationGroup { ctx in + ctx.duration = TimeInterval(duration) + window.animator().alphaValue = 0.0 + } completionHandler: { + window.close() + } + } + + // MARK: - Code-signing info match + +// @objc +// public func matchesCSInfo(_ csInfo1: NSDictionary?, _ csInfo2: NSDictionary?) -> Bool { +// var status1 = -1, status2 = -1 +// var signer1 = -1, signer2 = -1 +// var id1: String?, id2: String? +// var auths1: [Any]?, auths2: [Any]? +// +// if let n = csInfo1?[KEY_CS_STATUS] as? NSNumber { status1 = n.intValue } +// if let n = csInfo2?[KEY_CS_STATUS] as? NSNumber { status2 = n.intValue } +// if status1 != status2 { +// os_log_error(logHandle, "ERROR: code signing mismatch (signing status): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2)) +// return false +// } +// +// if let n = csInfo1?[KEY_CS_SIGNER] as? NSNumber { signer1 = n.intValue } +// if let n = csInfo2?[KEY_CS_SIGNER] as? NSNumber { signer2 = n.intValue } +// if signer1 != signer2 { +// if (signer1 == Apple && signer2 == AppStore) || (signer1 == AppStore && signer2 == Apple) { +// os_log_error(logHandle, "ignoring case where Apple App moved to/from Mac App Store: %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2)) +// } else { +// os_log_error(logHandle, "ERROR: code signing mismatch (signer): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2)) +// return false +// } +// } +// +// if let s = csInfo1?[KEY_CS_ID] as? String { id1 = s } +// if let s = csInfo2?[KEY_CS_ID] as? String { id2 = s } +// if (id1 != nil || id2 != nil), id1 != id2 { +// os_log_error(logHandle, "ERROR: code signing mismatch (signing ID): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2)) +// return false +// } +// +// if let a = csInfo1?[KEY_CS_AUTHS] as? [Any] { auths1 = a } +// if let a = csInfo2?[KEY_CS_AUTHS] as? [Any] { auths2 = a } +// if (auths1 != nil || auths2 != nil) && !(auths1 as NSArray? ?? []).isEqual(to: auths2 ?? []) { +// os_log_error(logHandle, "ERROR: code signing mismatch (signing auths): %{public}@ / %{public}@", String(describing: csInfo1), String(describing: csInfo2)) +// return false +// } +// +// return true +// } + + // MARK: - Escape JSON + + @objc + public func toEscapedJSON(_ input: String) -> String? { + do { + let data = try JSONSerialization.data(withJSONObject: input, options: .fragmentsAllowed) + return String(data: data, encoding: .utf8) + } catch { +// os_log_error(logHandle, "ERROR: failed to convert/escape %{public}@ to JSON (error: %{public}@)", input, error.localizedDescription) + return nil + } + } + + // MARK: - Absolute date from HH:mm (next 24h) + + @objc + public func absoluteDate(_ date: Date) -> Date { +// os_log_debug(logHandle, "function '%{public}s' invoked with %{public}@", #function, String(describing: date)) + + let now = Date() + let cal = Calendar.current + + let comps = cal.dateComponents([.hour, .minute], from: date) + var nowComps = cal.dateComponents([.year, .month, .day, .hour, .minute], from: now) + nowComps.hour = comps.hour + nowComps.minute = comps.minute + + var absDate = cal.date(from: nowComps) ?? now + if absDate < now { + absDate = cal.date(byAdding: .day, value: 1, to: absDate) ?? absDate + } + return absDate + } + + // MARK: - Internal volume? + + @objc + public func isInternalProcess(_ path: String) -> Bool { + var isInternal: AnyObject? + do { + var url = URL(fileURLWithPath: path) + try (url as NSURL).getResourceValue(&isInternal, forKey: URLResourceKey.volumeIsInternalKey) + return (isInternal as? NSNumber)?.boolValue ?? false + } catch { +// os_log_error(logHandle, "ERROR: 'getResourceValue'/'NSURLVolumeIsInternalKey' failed with %@", error.localizedDescription) + return false + } + } + + +} + diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift new file mode 100644 index 0000000..0610f54 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/ReverseDomainMapper.swift @@ -0,0 +1,57 @@ +// +// ReverseDomainMapper.swift +// SelfControlExtension +// +// Created by Satendra Singh on 14/10/25. +// + +import Foundation + +final class ReverseDomainMapper { + static func reverseDNSUsingGetNameInfo(ipAddress: String) -> String? { + var addr = sockaddr_in() + addr.sin_len = UInt8(MemoryLayout.size) + addr.sin_family = sa_family_t(AF_INET) + inet_pton(AF_INET, ipAddress, &addr.sin_addr) + + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + + // Precompute length so we don't read from `addr` inside the closure. + let addrLen = socklen_t(addr.sin_len) + + let result: Int32 = withUnsafePointer(to: &addr) { ptr in + ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in + getnameinfo(saPtr, + addrLen, + &hostname, socklen_t(hostname.count), + nil, 0, + NI_NAMEREQD) + } + } + + guard result == 0 else { return nil } + return String(cString: hostname) + } +} + +enum Regex { + static let ipAddress = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + static let hostname = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" +} + +extension String { + var isValidIpAddress: Bool { + return self.matches(pattern: Regex.ipAddress) + } + + var isValidHostname: Bool { + return self.matches(pattern: Regex.hostname) + } + + private func matches(pattern: String) -> Bool { + return self.range(of: pattern, + options: .regularExpression, + range: nil, + locale: nil) != nil + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift new file mode 100644 index 0000000..f652a95 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/TLDURLToDomain.swift @@ -0,0 +1,49 @@ +// +// TLDURLToDomain.swift +// SelfControlExtension +// +// Created by Satendra Singh on 15/10/25. +// + +import Foundation + +final class TLDURLToDomain { + func domain(for tldURL: URL) -> String? { + return tldURL.host + } + + private func extractDomain(from url: URL) -> String? { + return url.host + } + + + func getDomain(from urlString: String) -> String? { + guard let url = URL(string: urlString) else { + print("Invalid URL string.") + return nil + } + + // Using URL.host + if let host = url.host { + return host + } + return nil + } + + static func getURLDomain(from input: String) -> String? { + // Normalize input to ensure it has a scheme + let formatted = input.contains("://") ? input : "https://\(input)" + guard let host = URL(string: formatted)?.host else { return nil } + + let parts = host.split(separator: ".") + guard parts.count >= 2 else { return host } + + // Return last two parts (e.g., facebook.com, google.co.uk) + if parts.count >= 3 && parts[parts.count - 2] == "co" { + return parts.suffix(3).joined(separator: ".") // handles e.g. google.co.uk + } else { + return parts.suffix(2).joined(separator: ".") + } + } + +} From 0c7a066e0cbd42ab9d3a973917f53200785a607d Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Tue, 21 Oct 2025 16:40:50 +0530 Subject: [PATCH 3/9] added chrome extension --- .../SafariExtension/BlockListViewModel.swift | 37 ++++ .../BlockOrAllowList.swift | 147 +++++++++++++ .../dynamic-url-blocker/background.js | 199 ++++++++++++++++++ .../dynamic-url-blocker/manifest.json | 23 ++ .../dynamic-url-blocker/popup.css | 52 +++++ .../dynamic-url-blocker/popup.html | 24 +++ .../dynamic-url-blocker/popup.js | 73 +++++++ 7 files changed, 555 insertions(+) create mode 100644 Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift create mode 100644 Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift create mode 100644 Self-Control-Extension/dynamic-url-blocker/background.js create mode 100644 Self-Control-Extension/dynamic-url-blocker/manifest.json create mode 100644 Self-Control-Extension/dynamic-url-blocker/popup.css create mode 100644 Self-Control-Extension/dynamic-url-blocker/popup.html create mode 100644 Self-Control-Extension/dynamic-url-blocker/popup.js diff --git a/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift new file mode 100644 index 0000000..b8b2952 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl/SafariExtension/BlockListViewModel.swift @@ -0,0 +1,37 @@ +import Foundation +import Combine +import SafariServices + +/// ViewModel to manage blocked paths and write the blockerList.json into the App Group container. +class BlockListViewModel: ObservableObject { + @Published var blockedPaths: [String] = [ + "facebook.com/friends", + "youtube.com/shorts/", + "example.com/private/" + ] + private let appGroup = "group.com.application.SelfControl.corebits" + private let extensionIdentifier = "com.application.SelfControl.corebits.SelfControl-Safari-Extension" + + func addSample() { + blockedPaths.append("example.com/private/") + } + + func remove(at index: Int) { + guard index >= 0 && index < blockedPaths.count else { return } + blockedPaths.remove(at: index) + } + + func resetToDefaults() { + blockedPaths = [ + "facebook.com/friends", + "youtube.com/shorts/", + "example.com/private/" + ] + } + + func updateBlocker() { + let urls = ProxyPreferences.getBlockedDomains() + print("URLS: \(urls)") + BlockListManager.updateSafariBlockList(blockedPaths: urls, appGroup: appGroup, extensionIdentifier: extensionIdentifier) + } +} diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift new file mode 100644 index 0000000..5a1b065 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/BlockOrAllowList.swift @@ -0,0 +1,147 @@ +// +// BlockOrAllowList.swift +// SelfControl +// +// Created by Satendra Singh on 24/09/25. +// + + +import Foundation +import os.log +import NetworkExtension + +class BlockOrAllowList: NSObject { + + // MARK: - Properties + + var items: Set +// var lastModified: Date? + + // Serial queue for thread safety (replace @synchronized) +// private let queue = DispatchQueue(label: "BlockOrAllowList.serial") + + // MARK: - Initializer + + init(items: [String]) { + self.items = Set(items) + super.init() + } + + // MARK: - Methods + +// var isRemote: Bool { +// return path.hasPrefix("http://") || path.hasPrefix("https://") +// } + +// func load(_ path: String) { +// queue.sync { +// self.path = path +// self.items.removeAll() +// +// guard !path.isEmpty else { +// os_log("no list specified...", log: .default, type: .debug) +// return +// } +// +// var listString: String? +// var error: Error? +// +// if isRemote { +// os_log("(re)loading (remote) list", log: .default, type: .debug) +// if let url = URL(string: path) { +// do { +// listString = try String(contentsOf: url, encoding: .utf8) +// } catch let e { +// error = e +// } +// } +// if let error = error { +// os_log("ERROR: failed to (re)load (remote) list, %{public}@ (error: %{public}@)", log: .default, type: .error, path, String(describing: error)) +// return +// } +// // (Re)load remote URL once a day +// DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 24*60*60) { +// self.load(self.path) +// } +// } else { +// os_log("(re)loading (local) list, %{public}@", log: .default, type: .debug, path) +// do { +// listString = try String(contentsOfFile: path, encoding: .utf8) +// } catch let e { +// error = e +// } +// if let error = error { +// os_log("ERROR: failed to (re)load (local) list, %{public}@ (error: %{public}@)", log: .default, type: .error, path, String(describing: error)) +// return +// } +// if let attrs = try? FileManager.default.attributesOfItem(atPath: path), +// let fileModified = attrs[.modificationDate] as? Date { +// self.lastModified = fileModified +// } +// } +// +// if let listString = listString { +// let lines = listString.components(separatedBy: .newlines) +// let filtered = lines.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } +// .filter { !$0.isEmpty && !$0.hasPrefix("#") } +// self.items = Set(filtered) +// os_log("SC] 🔍(re)loaded %lu list items", log: .default, type: .debug, self.items.count) +// } +// } +// } + + /// Check if flow matches item on block or allow list + func isMatch(_ flow: NEFilterSocketFlow) -> Bool { + // Only access properties and perform mutation in the queue (thread safety) +// return queue.sync { + + var endpointNames = Set() + + // Get remote endpoint host + if let url = flow.url, let urlString = url.absoluteString.lowercased() as String? { + endpointNames.insert(urlString) + } + if let url = flow.url, let host = url.host?.lowercased() { + endpointNames.insert(host) + } + if let remoteEndpoint = flow.remoteEndpoint as? NWHostEndpoint { + endpointNames.insert(remoteEndpoint.hostname.lowercased()) + } + + // macOS 11+ specific property + if #available(macOS 11, *) { + if let remoteHostname = flow.remoteHostname?.lowercased() { + endpointNames.insert(remoteHostname) + if remoteHostname.hasPrefix("www.") { + let noWWW = String(remoteHostname.dropFirst(4)) + endpointNames.insert(noWWW) + } + } + } + os_log("[SC] 🔍] BlockList endpoint names : %{public}@", log: OSLog.default, type: .info, endpointNames.debugDescription) + + // Find matches + let matches = items.intersection(endpointNames) + if !matches.isEmpty { + os_log("SC] 🔍 endpoint names %{public}@ matched the following list items %{public}@", log: .default, type: .debug, String(describing: endpointNames), String(describing: matches)) + return true + } + + return false +// } + } +} + +// MARK: - NEFilterSocketFlow Extensions + +//extension NEFilterSocketFlow { +// /// You must implement these extensions if your Objective-C code provides them! +// @objc public override var url: URL? { +// // Implement according to your codebase (category/extension in Objective-C) +// return nil // Placeholder +// } +// @objc var remoteHostname: String? { +// // Implement according to your codebase (category/extension in Objective-C) +// return nil // Placeholder +// } +//} diff --git a/Self-Control-Extension/dynamic-url-blocker/background.js b/Self-Control-Extension/dynamic-url-blocker/background.js new file mode 100644 index 0000000..3625f6a --- /dev/null +++ b/Self-Control-Extension/dynamic-url-blocker/background.js @@ -0,0 +1,199 @@ + +// Dynamic URL Blocker — background service worker (Manifest V3) + +// ---- Helpers ---- +function normalizeDomain(input) { + // strip protocol, path, query, hash + let domain = (input || "").trim().toLowerCase(); + domain = domain.replace(/^https?:\/\//, ""); + domain = domain.replace(/^www\./, ""); + domain = domain.split("/")[0].split("?")[0].split("#")[0]; + // allow punycode etc. Keep only allowed domain chars, dots, and dashes + domain = domain.replace(/[^a-z0-9\.\-]/g, ""); + return domain; +} + +function escapeRegex(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +// Build a regex that matches the domain and any subdomain, main-frame only. +function regexForDomain(domain) { + const d = escapeRegex(domain); + // ^https?://([sub.]* )?domain(/|:|$) + return `^https?:\\/\\/([a-z0-9-]+\\.)*${d}(\\/|:|$)`; +} + +// Simple deterministic hash for rule IDs (32-bit positive) +function hash32(str) { + let h = 5381; + for (let i = 0; i < str.length; i++) { + h = ((h << 5) + h) ^ str.charCodeAt(i); // djb2 xor + h = h | 0; // force 32-bit + } + // make positive and avoid 0 & small ids + h = Math.abs(h); + return (h % 2000000000) + 1000; // keep under Chrome's int32 max +} + +function ruleForDomain(domain) { + return { + id: hash32(domain), + priority: 1, + action: { type: "block" }, + condition: { + regexFilter: regexForDomain(domain), + resourceTypes: ["main_frame"] + } + }; +} + +async function fetchData() { + // try { + // const response = await fetch("http://127.0.0.1:8080/"); + // const data = await response.json(); + // console.log("Swift app data:", data); + // } catch (err) { + // console.error("Error fetching from Swift app:", err); + // } + try { + const response = await fetch("http://127.0.0.1:8080/"); + const data = await response.json(); + console.log("Array from Swift:", data); + + // // iterate + // data.forEach(item => { + // console.log(`${item. } is ${item.status}`); + // }); + // Parse into JS object +// const data = JSON.parse(jsonString); + +// Access blocked domain list +const blockedDomains = data.blocked; + +console.log(blockedDomains); +// 👉 ["domain1.com", "domain2.com", "domain3.com"] + +// Example: loop over them +blockedDomains.forEach(domain => { + console.log("Blocked:", domain); +}); + return blockedDomains; + } catch (err) { + console.error("Fetch failed:", err); + return []; + } +} + + +async function getDynamicRuleIds() { + const rules = await chrome.declarativeNetRequest.getDynamicRules(); + return rules.map(r => r.id); +} + +async function applyRulesFrom(blockedDomains) { + const desiredRules = blockedDomains.map(ruleForDomain); + const desiredIds = new Set(desiredRules.map(r => r.id)); + const currentIds = new Set(await getDynamicRuleIds()); + + // Remove any rules that shouldn't exist + const removeRuleIds = [...currentIds].filter(id => !desiredIds.has(id)); + + // Add rules that don't yet exist + const addRules = desiredRules.filter(r => !currentIds.has(r.id)); + + await chrome.declarativeNetRequest.updateDynamicRules({ + removeRuleIds, + addRules + }); +} + +async function loadBlockedDomains() { + const domains = await fetchData(); + return domains; + // return new Promise(resolve => { + // blockedDomains + // chrome.storage.local.get({ blockedSites: [] }, (data) => { + // resolve(data.blockedSites || []); + // }); + // }); +} + +async function saveBlockedDomains(domains) { + return new Promise(resolve => { + chrome.storage.local.set({ blockedSites: domains }, () => resolve()); + }); +} + +// ---- Lifecycle ---- +chrome.runtime.onInstalled.addListener(async () => { + const domains = await loadBlockedDomains(); + await applyRulesFrom(domains); + console.log("Dynamic URL Blocker installed. Domains:", domains); + // fetchData(); +}); + +chrome.tabs.onCreated.addListener((tab) => { + (async () => { + console.log("New tab:", tab.id); + const domains = await loadBlockedDomains(); + await applyRulesFrom(domains); + // Example: get tab info with async API + let tabInfo = await chrome.tabs.get(tab.id); + console.log("Fetched tab info:", tabInfo); + })(); +}); + +chrome.windows.onCreated.addListener((window) => { + (async () => { + const domains = await loadBlockedDomains(); + await applyRulesFrom(domains); + // Example: get tab info with async API + let windowInfo = await chrome.tabs.get(window.id); + console.log("New Window created:", windowInfo); + })(); +}); + +// ---- Messaging API for popup ---- +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + (async () => { + if (message?.action === "list") { + const domains = await loadBlockedDomains(); + sendResponse({ success: true, domains }); + return; + } + + if (message?.action === "add") { + let domain = normalizeDomain(message.site || ""); + if (!domain) { + sendResponse({ success: false, error: "Please enter a valid domain (e.g., example.com)." }); + return; + } + let domains = await loadBlockedDomains(); + if (!domains.includes(domain)) { + domains.push(domain); + await saveBlockedDomains(domains); + await applyRulesFrom(domains); + sendResponse({ success: true, domains }); + } else { + sendResponse({ success: false, error: "Already blocked." }); + } + return; + } + + if (message?.action === "remove") { + const domain = normalizeDomain(message.site || ""); + let domains = await loadBlockedDomains(); + domains = domains.filter(d => d !== domain); + await saveBlockedDomains(domains); + await applyRulesFrom(domains); + sendResponse({ success: true, domains }); + return; + } + + sendResponse({ success: false, error: "Unknown action." }); + })(); + + // Indicate we'll respond asynchronously + return true; +}); diff --git a/Self-Control-Extension/dynamic-url-blocker/manifest.json b/Self-Control-Extension/dynamic-url-blocker/manifest.json new file mode 100644 index 0000000..e64a8f3 --- /dev/null +++ b/Self-Control-Extension/dynamic-url-blocker/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "Dynamic URL Blocker", + "version": "1.0.0", + "description": "Block websites by typing them into a popup. Uses dynamic Declarative Net Request rules.", + "permissions": [ + "declarativeNetRequest", + "storage", + "scripting" + ], + "host_permissions": [ + "http://localhost/*", + "http://127.0.0.1/*" + ], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "action": { + "default_popup": "popup.html", + "default_title": "Dynamic URL Blocker" + } +} \ No newline at end of file diff --git a/Self-Control-Extension/dynamic-url-blocker/popup.css b/Self-Control-Extension/dynamic-url-blocker/popup.css new file mode 100644 index 0000000..d856722 --- /dev/null +++ b/Self-Control-Extension/dynamic-url-blocker/popup.css @@ -0,0 +1,52 @@ + +body { + font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; + padding: 12px; + width: 320px; +} + +h2 { margin: 0 0 8px; font-size: 16px; } +h3 { margin: 12px 0 6px; font-size: 14px; } + +.row { display: flex; gap: 8px; } + +input { + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 8px; +} + +button { + padding: 8px 12px; + border: none; + border-radius: 8px; + background: #111827; + color: white; + cursor: pointer; +} + +button:hover { opacity: 0.9; } + +ul { list-style: none; padding: 0; margin: 0; } + +li { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; +} + +li .domain { font-size: 13px; } + +li button.remove { + background: transparent; + color: #ef4444; + border: 1px solid #ef4444; + border-radius: 6px; + padding: 2px 8px; +} + +.hint { color: #666; font-size: 12px; } +.error { color: #b91c1c; font-size: 12px; margin-top: 6px; } diff --git a/Self-Control-Extension/dynamic-url-blocker/popup.html b/Self-Control-Extension/dynamic-url-blocker/popup.html new file mode 100644 index 0000000..abb3a07 --- /dev/null +++ b/Self-Control-Extension/dynamic-url-blocker/popup.html @@ -0,0 +1,24 @@ +\ + + + + + Dynamic URL Blocker + + + + + +

Blocks the domain and all its subdomains.

+ +

Blocked Domains

+
    + + + + diff --git a/Self-Control-Extension/dynamic-url-blocker/popup.js b/Self-Control-Extension/dynamic-url-blocker/popup.js new file mode 100644 index 0000000..89c26a8 --- /dev/null +++ b/Self-Control-Extension/dynamic-url-blocker/popup.js @@ -0,0 +1,73 @@ + +const siteInput = document.getElementById("siteInput"); +const addBtn = document.getElementById("addBtn"); +const siteList = document.getElementById("siteList"); + +function normalizeDomain(input) { + let domain = (input || "").trim().toLowerCase(); + domain = domain.replace(/^https?:\/\//, ""); + domain = domain.replace(/^www\./, ""); + domain = domain.split("/")[0].split("?")[0].split("#")[0]; + domain = domain.replace(/[^a-z0-9\.\-]/g, ""); + return domain; +} + +function render(list) { + siteList.innerHTML = ""; + if (!list || list.length === 0) { + const li = document.createElement("li"); + li.innerHTML = 'No blocked domains yet.'; + siteList.appendChild(li); + return; + } + + list.forEach(site => { + const li = document.createElement("li"); + + const span = document.createElement("span"); + span.className = "domain"; + span.textContent = site; + + // const removeBtn = document.createElement("button"); + // removeBtn.className = "remove"; + // removeBtn.textContent = "Unblock"; + // removeBtn.onclick = () => { + // chrome.runtime.sendMessage({ action: "remove", site }, (resp) => { + // render(resp.domains || []); + // }); + // }; + + li.appendChild(span); + // li.appendChild(removeBtn); + siteList.appendChild(li); + }); +} + +function load() { + chrome.runtime.sendMessage({ action: "list" }, (resp) => { + render(resp.domains || []); + }); +} + +// addBtn.onclick = () => { +// const raw = siteInput.value; +// const site = normalizeDomain(raw); +// if (!site) { +// alert("Please enter a valid domain, e.g. example.com"); +// return; +// } +// chrome.runtime.sendMessage({ action: "add", site }, (resp) => { +// if (!resp?.success) { +// alert(resp?.error || "Failed to add."); +// return; +// } +// siteInput.value = ""; +// render(resp.domains || []); +// }); +// }; + +// siteInput.addEventListener("keydown", (e) => { +// if (e.key === "Enter") addBtn.click(); +// }); + +document.addEventListener("DOMContentLoaded", load); From 1bc6fb4082b27efb1ba6d6e17cc6ba353f470687 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Sun, 26 Oct 2025 14:31:36 +0530 Subject: [PATCH 4/9] UPDATED build settings --- .../SelfControl.xcodeproj/project.pbxproj | 71 ++++++++++--------- .../SelfControl/SelfControl.entitlements | 9 +-- .../SelfControlExtension.entitlements | 6 +- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj index 088b18a..4d87aca 100644 --- a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj +++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 9A7FFB6D2E950837007D4A0D /* Exceptions for "SelfControl Safari Extension" folder in "SelfControl Safari Extension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + block.html, Info.plist, ); target = 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */; @@ -485,24 +486,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SelfControl/SelfControl.entitlements; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\""; - DEVELOPMENT_TEAM = X6FQ433AWK; - ENABLE_APP_SANDBOX = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SelfControl/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -514,6 +507,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control app dev"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -525,24 +520,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = SelfControl/SelfControl.entitlements; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"SelfControl/Preview Content\""; - DEVELOPMENT_TEAM = X6FQ433AWK; - ENABLE_APP_SANDBOX = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = SelfControl/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -554,6 +541,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control app dev"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; @@ -563,9 +552,12 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = SelfControlExtension/SelfControlExtension.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = X6FQ433AWK; + "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SelfControlExtension/Info.plist; @@ -583,6 +575,7 @@ OTHER_LDFLAGS = "-lbsm"; PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network; PRODUCT_NAME = "$(inherited)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control Dev ext"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -593,9 +586,12 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = SelfControlExtension/SelfControlExtension.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = X6FQ433AWK; + "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SelfControlExtension/Info.plist; @@ -613,6 +609,7 @@ OTHER_LDFLAGS = "-lbsm"; PRODUCT_BUNDLE_IDENTIFIER = com.application.SelfControl.corebits.network; PRODUCT_NAME = "$(inherited)"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Self Control Dev ext"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -623,12 +620,13 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "SelfControl Safari Extension/SelfControl Safari Extension.entitlements"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = X6FQ433AWK; - ENABLE_APP_SANDBOX = YES; + "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension"; @@ -642,6 +640,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.application.SelfControl.corebits.SelfControl-Safari-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "SelfControl Dev Net Ext"; REGISTER_APP_GROUPS = YES; SKIP_INSTALL = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -656,12 +656,13 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "SelfControl Safari Extension/SelfControl Safari Extension.entitlements"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = X6FQ433AWK; - ENABLE_APP_SANDBOX = YES; + "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension"; @@ -675,6 +676,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.application.SelfControl.corebits.SelfControl-Safari-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "SelfControl Dev Net Ext"; REGISTER_APP_GROUPS = YES; SKIP_INSTALL = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements index 9568570..9cc108e 100644 --- a/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements +++ b/Self-Control-Extension/SelfControl/SelfControl/SelfControl.entitlements @@ -4,21 +4,14 @@ com.apple.developer.networking.networkextension - url-filter-provider - content-filter-provider + content-filter-provider-systemextension com.apple.developer.system-extension.install - com.apple.security.app-sandbox - com.apple.security.application-groups group.com.application.SelfControl.corebits $(TeamIdentifierPrefix)com.application.SelfControl.corebits - com.apple.security.files.user-selected.read-only - - com.apple.security.network.server - diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements b/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements index 969c744..f6ded53 100644 --- a/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/SelfControlExtension.entitlements @@ -4,15 +4,11 @@ com.apple.developer.networking.networkextension - content-filter-provider + content-filter-provider-systemextension - com.apple.security.app-sandbox - com.apple.security.application-groups $(TeamIdentifierPrefix)com.application.SelfControl.corebits - com.apple.security.network.server - From 5801472a0dd3feed885ed3d7a952db8ded56a1f9 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Sun, 26 Oct 2025 14:32:13 +0530 Subject: [PATCH 5/9] implemented SNI parser --- .../FilterDataProvider.swift | 182 ++++++++++++++++-- 1 file changed, 164 insertions(+), 18 deletions(-) diff --git a/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift index f0ea001..3cd3f4c 100644 --- a/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift +++ b/Self-Control-Extension/SelfControl/SelfControlExtension/FilterDataProvider.swift @@ -108,9 +108,155 @@ class FilterDataProvider: NEFilterDataProvider { return nil } + func extractHost(from request: String) -> String? { + for line in request.split(separator: "\r\n") { + if line.lowercased().hasPrefix("host:") { + return line.replacingOccurrences(of: "Host:", with: "", options: .caseInsensitive) + .trimmingCharacters(in: .whitespaces) + } + } + return nil + } + + func extractPath(from request: String) -> String? { + guard let firstLine = request.split(separator: "\r\n").first else { return nil } + let comps = firstLine.split(separator: " ") + return comps.count > 1 ? String(comps[1]) : nil + } + + func extractSNI(fromTLSData data: Data) -> String? { + // TLS record starts with 0x16 (Handshake), version bytes, then length + guard data.count > 5, data[0] == 0x16 else { return nil } + + var offset = 5 // Skip record header + + // Verify handshake type = ClientHello (0x01) + guard data.count > offset, data[offset] == 0x01 else { return nil } + offset += 4 // Skip type + length (3 bytes) + + // Skip protocol version (2), random (32), session id length + session id + guard data.count > offset + 34 else { return nil } + offset += 34 + guard data.count > offset else { return nil } + + // Skip session ID + if offset < data.count { + let sessionIDLength = Int(data[offset]) + offset += 1 + sessionIDLength + } + + // Skip cipher suites + guard offset + 2 <= data.count else { return nil } + let cipherSuiteLength = Int(data[offset]) << 8 | Int(data[offset + 1]) + offset += 2 + cipherSuiteLength + + // Skip compression methods + guard offset < data.count else { return nil } + let compressionLength = Int(data[offset]) + offset += 1 + compressionLength + + // Skip extensions length + guard offset + 2 <= data.count else { return nil } + let extensionsLength = Int(data[offset]) << 8 | Int(data[offset + 1]) + offset += 2 + guard offset + extensionsLength <= data.count else { return nil } + + var extOffset = offset + while extOffset + 4 <= offset + extensionsLength { + let extType = Int(data[extOffset]) << 8 | Int(data[extOffset + 1]) + let extLen = Int(data[extOffset + 2]) << 8 | Int(data[extOffset + 3]) + extOffset += 4 + + // Extension type 0 = Server Name + if extType == 0 { + // Parse Server Name extension + var nameOffset = extOffset + 2 // skip list length + while nameOffset + 3 < extOffset + extLen { + let nameType = data[nameOffset] + let nameLen = Int(data[nameOffset + 1]) << 8 | Int(data[nameOffset + 2]) + nameOffset += 3 + if nameType == 0, nameOffset + nameLen <= data.count { + let nameData = data.subdata(in: nameOffset ..< nameOffset + nameLen) + return String(data: nameData, encoding: .utf8) + } + nameOffset += nameLen + } + } + + extOffset += extLen + } + + return nil + } + + override func handleOutboundData( + from flow: NEFilterFlow, + readBytesStartOffset offset: Int, + readBytes data: Data + ) -> NEFilterDataVerdict { + + + let process = processFromflow(flow: flow) + // os_log(" FilterDataProvider: appIdAndName %{public}@, %{public}@", log: OSLog.default, type: .debug, process?.name ?? "", process?.path ?? "") + os_log(" FilterDataProvider: app %{public}@", log: OSLog.default, type: .debug, process?.description ?? "") + + if let process = process, AllowedProcess.isAllowedProcess(process) { + os_log("[SC] 🔍] FilterDataProvider: allowing as it is in allowed list.") + return .allow() + } + guard let socketFlow = flow as? NEFilterSocketFlow else { + os_log("[SC] 🔍] Not a socket flow. Allowing.", log: OSLog.default, type: .info) + return .allow() + } + if socketFlow.direction != .outbound { + os_log("[SC] 🔍] Not a inbound socket flow. Allowing.", log: OSLog.default, type: .info) + return .allow() + } + +// guard let socketFlow = flow as? NEFilterSocketFlow else { +// return .allow() +// } + let requestString = String(data: data, encoding: .utf8) + if let requestString { + os_log("[SC] 🔍] data HTTP Request: %{public}@", requestString) + } + + // Try HTTP detection + if let requestString = requestString, + requestString.hasPrefix("GET ") || requestString.hasPrefix("POST ") { + + if let host = extractHost(from: requestString), + let path = extractPath(from: requestString) { + let urlString = "http://\(host)\(path)" + os_log("[SC] 🔍] data HTTP Request: %{public}@", urlString) + } + + } else if let sni = extractSNI(fromTLSData: data) { + os_log("[SC] 🔍] data TLS SNI Host: %{public}@", sni) + guard let hostDomain = TLDURLToDomain.getURLDomain(from: sni) else { + return .allow() + } + os_log("[SC] 🔍] data hostDomain: %{public}@", hostDomain) + + for url in IPCConnection.shared.blockedUrls { + if url.contains(hostDomain) { + os_log("[SC] 🔍] data Blocking flow Host match SNI:%{public}@, host:%{public}@", sni, hostDomain) + return .drop() + } + } + } + + return .allow() + } // Called for each new flow. override func handleNewFlow(_ flow: NEFilterFlow) -> NEFilterNewFlowVerdict { + // Provide peek sizes required by this overload + return .filterDataVerdict(withFilterInbound: false, + peekInboundBytes: 0, + filterOutbound: true, + peekOutboundBytes: Int.max) + os_log("[SC] 🔍] FilterDataProvider: handleNewFlow invoked", log: OSLog.default, type: .debug) // if let appID = flow.sourceAppIdentifier { // print("App making the request: \(appID)") @@ -269,24 +415,24 @@ class FilterDataProvider: NEFilterDataProvider { return completionHandler(true) } - override func handleOutboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict { - if let urlString = flow.url?.absoluteString { - os_log("[SC] 🔍] handleOutboundData URL: %{public}@", urlString) - if urlString.contains("facebook.com/friends") { - return .drop() - } - } - - if let requestString = String(data: readBytes, encoding: .utf8) { - print("Outbound data: \(requestString)") - os_log("[SC] 🔍] handleOutboundData: %{public}@", requestString) - - if requestString.contains("facebook.com/friends") { - return .drop() - } - } - return .allow() - } +// override func handleOutboundData(from flow: NEFilterFlow, readBytesStartOffset offset: Int, readBytes: Data) -> NEFilterDataVerdict { +// if let urlString = flow.url?.absoluteString { +// os_log("[SC] 🔍] handleOutboundData URL: %{public}@", urlString) +// if urlString.contains("facebook.com/friends") { +// return .drop() +// } +// } +// +// if let requestString = String(data: readBytes, encoding: .utf8) { +// print("Outbound data: \(requestString)") +// os_log("[SC] 🔍] handleOutboundData: %{public}@", requestString) +// +// if requestString.contains("facebook.com/friends") { +// return .drop() +// } +// } +// return .allow() +// } // override func handleOutboundData(from flow: NEFilterFlow, // readBytesStartOffset offset: Int, From 4fd5ce0593a73ee24355ef2347d0093b7fb972d4 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Tue, 28 Oct 2025 19:42:48 +0530 Subject: [PATCH 6/9] enable signing for --- .../SelfControl.xcodeproj/project.pbxproj | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj index 4d87aca..2882203 100644 --- a/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj +++ b/Self-Control-Extension/SelfControl/SelfControl.xcodeproj/project.pbxproj @@ -93,7 +93,6 @@ 9A7FFB6D2E950837007D4A0D /* Exceptions for "SelfControl Safari Extension" folder in "SelfControl Safari Extension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - block.html, Info.plist, ); target = 9A7FFB5E2E950837007D4A0D /* SelfControl Safari Extension */; @@ -625,8 +624,19 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = X6FQ433AWK; "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; - ENABLE_APP_SANDBOX = NO; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension"; @@ -661,8 +671,19 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = X6FQ433AWK; "DEVELOPMENT_TEAM[sdk=macosx*]" = X6FQ433AWK; - ENABLE_APP_SANDBOX = NO; + ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "SelfControl Safari Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SelfControl Safari Extension"; From 28ccd6da5e0e04f721e582348a231dff94e8eeec Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Tue, 28 Oct 2025 19:43:07 +0530 Subject: [PATCH 7/9] updated log files --- .../ContentBlockerExtensionRequestHandler.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift index 9c4ba46..b20bd5f 100644 --- a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerExtensionRequestHandler.swift @@ -18,7 +18,7 @@ public enum ContentBlockerExtensionRequestHandler { /// - context: The extension context that initiated the request. /// - groupIdentifier: The app group identifier used to access the shared container. public static func handleRequest(with context: NSExtensionContext, groupIdentifier: String) { - os_log(.info, "Start loading the content blocker") + os_log(.info, "[SC] 🔍] Safari Start loading the content blocker, %{public}@", context.inputItems.description) // Get the shared container URL using the provided group identifier guard @@ -38,7 +38,7 @@ public enum ContentBlockerExtensionRequestHandler { // Determine which blocker list file to use var blockerListFileURL = sharedFileURL if !FileManager.default.fileExists(atPath: sharedFileURL.path) { - os_log(.info, "No blocker list file found. Using the default one.") + os_log(.info, "[SC] 🔍] Safari No blocker list file found. Using the default one.") // Fall back to the default blocker list included in the bundle guard @@ -47,7 +47,7 @@ public enum ContentBlockerExtensionRequestHandler { context.cancelRequest( withError: createError( code: 1002, - message: "Failed to find default blocker list." + message: "[SC] 🔍] Safari Failed to find default blocker list." ) ) return @@ -66,11 +66,13 @@ public enum ContentBlockerExtensionRequestHandler { // Prepare and complete the extension request with the blocker list let item = NSExtensionItem() item.attachments = [attachment] - +// item.attributedTitle = NSAttributedString(string: "Hellow world!") +// item.attributedContentText = NSAttributedString(string: "Hello Content of the world!") + context.completeRequest( returningItems: [item] ) { _ in - os_log(.info, "Finished loading the content blocker") + os_log(.info, "[SC] 🔍] Safari Finished loading the content blocker") } } From aca78c07083c3633491e76fdca2165d54d7b8c96 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Tue, 28 Oct 2025 19:43:27 +0530 Subject: [PATCH 8/9] added logs in safari ext --- .../ContentBlockerRequestHandler.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift index 90a5176..e2fb7c4 100644 --- a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/ContentBlockerRequestHandler.swift @@ -7,6 +7,7 @@ import Foundation import UniformTypeIdentifiers +import os.log class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling { // Must match the App Group used by the host app @@ -48,6 +49,7 @@ class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling { let fileManager = FileManager.default guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { NSLog("❌ App Group container not found for %@", appGroup) + os_log("[SC] 🔍] Safari ❌ App Group container not found for: %{public}@", appGroup) return nil } return containerURL.appendingPathComponent(sharedFileName) @@ -60,7 +62,17 @@ class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling { try data.write(to: url, options: .atomic) } catch { NSLog("❌ Failed to write temporary rules file: \(error.localizedDescription)") + os_log("[SC] 🔍] Safari ❌ Failed to write temporary rules file: %{public}@", error.localizedDescription) + } return url } + + + func cancelRequest(withError error: any Error) { + + os_log("[SC] 🔍] Safari ❌ cancelRequeste: %{public}@", error.localizedDescription) + + } + } From bebf74e3c6f107b124525f8852dc32dc081ee956 Mon Sep 17 00:00:00 2001 From: Satendra Singh Date: Tue, 28 Oct 2025 19:43:44 +0530 Subject: [PATCH 9/9] added block html --- .../SelfControl Safari Extension/block.html | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html diff --git a/Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html new file mode 100644 index 0000000..77a1572 --- /dev/null +++ b/Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html @@ -0,0 +1,33 @@ + + + + +Site Blocked + + + + +

    🚫 Site Blocked

    +

    This website has been blocked by your Safari content blocker.

    +

    You can change this in Safari ▸ Extensions if you want to allow it.

    + Open Safari Extensions + +