diff --git a/.changeset/old-cheetahs-watch.md b/.changeset/old-cheetahs-watch.md new file mode 100644 index 0000000..7a71005 --- /dev/null +++ b/.changeset/old-cheetahs-watch.md @@ -0,0 +1,5 @@ +--- +"@softnetics/dotlocal": minor +--- + +Switch from OrbStack to dns-sd diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bed4d68..4b976ed 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,7 @@ name: build on: push: branches: - - main + - '**' tags: - "v*.*.*" @@ -18,7 +18,7 @@ permissions: repository-projects: read security-events: read statuses: read - + jobs: build: runs-on: macos-13 diff --git a/.gitignore b/.gitignore index dd3bdb1..3e0afb1 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,4 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ +default.profraw diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a153a33 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "clientcommon.h": "c" + } +} \ No newline at end of file diff --git a/Config/Config.xcconfig b/Config/Config.xcconfig index 9db76c8..7b78d89 100644 --- a/Config/Config.xcconfig +++ b/Config/Config.xcconfig @@ -9,3 +9,9 @@ // https://help.apple.com/xcode/#/dev745c5c974 #include "Version.xcconfig" + +APP_BUNDLE_IDENTIFIER = dev.suphon.DotLocal +HELPER_TOOL_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).helper + +APP_VERSION = $(MARKETING_VERSION) +HELPER_VERSION = $(MARKETING_VERSION) diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index cf00ead..e36c22d 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -8,12 +8,7 @@ /* Begin PBXBuildFile section */ D50377F62B481B59008F9AA8 /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = D50377F52B481B59008F9AA8 /* GRPC */; }; - D50378042B482578008F9AA8 /* dot-local.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378022B482578008F9AA8 /* dot-local.pb.swift */; }; - D50378052B482578008F9AA8 /* dot-local.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378012B482578008F9AA8 /* dot-local.grpc.swift */; }; - D50378062B482578008F9AA8 /* preferences.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378032B482578008F9AA8 /* preferences.pb.swift */; }; - D50378092B48708C008F9AA8 /* MappingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378082B48708C008F9AA8 /* MappingListViewModel.swift */; }; D503780B2B48718D008F9AA8 /* MappingList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503780A2B48718D008F9AA8 /* MappingList.swift */; }; - D503780D2B487250008F9AA8 /* ProtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503780C2B487250008F9AA8 /* ProtoExtensions.swift */; }; D529B1AE2B47BF8C00DC288B /* DotLocalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1AD2B47BF8C00DC288B /* DotLocalApp.swift */; }; D529B1B02B47BF8C00DC288B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1AF2B47BF8C00DC288B /* ContentView.swift */; }; D529B1B22B47BF8E00DC288B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D529B1B12B47BF8E00DC288B /* Assets.xcassets */; }; @@ -21,13 +16,48 @@ D529B1C02B47BF8E00DC288B /* DotLocalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1BF2B47BF8E00DC288B /* DotLocalTests.swift */; }; D529B1CA2B47BF8E00DC288B /* DotLocalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1C92B47BF8E00DC288B /* DotLocalUITests.swift */; }; D529B1CC2B47BF8E00DC288B /* DotLocalUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1CB2B47BF8E00DC288B /* DotLocalUITestsLaunchTests.swift */; }; - D582E9072B4C5BE20054343B /* nginx in Copy Binaries */ = {isa = PBXBuildFile; fileRef = D582E9052B4C5BC00054343B /* nginx */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + D56116AA2B510CFD00FEB087 /* DaemonManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116A92B510CFD00FEB087 /* DaemonManager.swift */; }; + D56116BC2B51621500FEB087 /* dot-local.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116B92B51621500FEB087 /* dot-local.pb.swift */; }; + D56116BD2B51621500FEB087 /* dot-local.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116B92B51621500FEB087 /* dot-local.pb.swift */; }; + D56116BE2B51621500FEB087 /* preferences.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116BA2B51621500FEB087 /* preferences.pb.swift */; }; + D56116BF2B51621500FEB087 /* preferences.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116BA2B51621500FEB087 /* preferences.pb.swift */; }; + D56116C02B51621500FEB087 /* dot-local.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116BB2B51621500FEB087 /* dot-local.grpc.swift */; }; + D56116C12B51621500FEB087 /* dot-local.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116BB2B51621500FEB087 /* dot-local.grpc.swift */; }; + D56116C32B51628E00FEB087 /* ProtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116C22B51628E00FEB087 /* ProtoExtensions.swift */; }; + D56116C42B51628E00FEB087 /* ProtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116C22B51628E00FEB087 /* ProtoExtensions.swift */; }; + D56116C62B517DC800FEB087 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116C52B517DC800FEB087 /* ViewExtensions.swift */; }; + D56116C72B517DC800FEB087 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116C52B517DC800FEB087 /* ViewExtensions.swift */; }; + D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5793C482B50E8D400979DC3 /* HelperManager.swift */; }; + D5793C5C2B5103CC00979DC3 /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = D5793C5B2B5103CC00979DC3 /* GRPC */; }; + D5793C5E2B5103CF00979DC3 /* EmbeddedPropertyList in Frameworks */ = {isa = PBXBuildFile; productRef = D5793C5D2B5103CF00979DC3 /* EmbeddedPropertyList */; }; + D5793C602B5103E300979DC3 /* DaemonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5793C5F2B5103E300979DC3 /* DaemonState.swift */; }; + D5793C612B5103E300979DC3 /* DaemonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5793C5F2B5103E300979DC3 /* DaemonState.swift */; }; + D5803ED02B5194CF00332743 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803ECF2B5194CF00332743 /* main.swift */; }; + D5803EDD2B51990100332743 /* HelperToolMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EDC2B51990100332743 /* HelperToolMonitor.swift */; }; + D5803EDE2B519ADA00332743 /* dev.suphon.DotLocal.helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = D59D894D2B4FFC380009270C /* dev.suphon.DotLocal.helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + D5803EE22B519E3B00332743 /* Blessed in Frameworks */ = {isa = PBXBuildFile; productRef = D5803EE12B519E3B00332743 /* Blessed */; }; + D5803EE42B519E4600332743 /* Blessed in Frameworks */ = {isa = PBXBuildFile; productRef = D5803EE32B519E4600332743 /* Blessed */; }; + D5803EE72B51A19500332743 /* Uninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EE62B51A19500332743 /* Uninstaller.swift */; }; + D5803EE92B51A1A200332743 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EE82B51A1A200332743 /* Updater.swift */; }; + D582E9072B4C5BE20054343B /* nginx in Copy Nginx */ = {isa = PBXBuildFile; fileRef = D582E9052B4C5BC00054343B /* nginx */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + D59D89502B4FFC380009270C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D894F2B4FFC380009270C /* main.swift */; }; + D59D89612B5048C40009270C /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89602B5048C40009270C /* SharedConstants.swift */; }; + D59D89622B5048C40009270C /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89602B5048C40009270C /* SharedConstants.swift */; }; + D59D89692B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */; }; + D59D896A2B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */; }; + D59D896E2B5055BB0009270C /* EmbeddedPropertyList in Frameworks */ = {isa = PBXBuildFile; productRef = D59D896D2B5055BB0009270C /* EmbeddedPropertyList */; }; + D59D89792B505C430009270C /* SecureXPC in Frameworks */ = {isa = PBXBuildFile; productRef = D59D89782B505C430009270C /* SecureXPC */; }; + D59D897B2B505C4B0009270C /* SecureXPC in Frameworks */ = {isa = PBXBuildFile; productRef = D59D897A2B505C4B0009270C /* SecureXPC */; }; + D59D897D2B505E4B0009270C /* CodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897C2B505E4B0009270C /* CodeInfo.swift */; }; + D59D897E2B505E4B0009270C /* CodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897C2B505E4B0009270C /* CodeInfo.swift */; }; + D59D89812B505EFE0009270C /* ManageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897F2B505EFE0009270C /* ManageClient.swift */; }; + D59D89862B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */; }; + D59D89872B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */; }; D5DEA9B32B4888310029BB00 /* AppMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DEA9B22B4888310029BB00 /* AppMenu.swift */; }; D5DEA9B62B49936B0029BB00 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D5DEA9B52B49936B0029BB00 /* LaunchAtLogin */; }; D5DEA9BC2B4995BC0029BB00 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = D5DEA9BB2B4995BC0029BB00 /* Defaults */; }; D5DEA9BE2B4995DD0029BB00 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */; }; D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DEA9BF2B499C2F0029BB00 /* Defaults.swift */; }; - D5DEA9C22B49A6B60029BB00 /* Sudo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DEA9C12B49A6B60029BB00 /* Sudo.swift */; }; D5DEA9C42B49C0200029BB00 /* ClientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DEA9C32B49C0200029BB00 /* ClientManager.swift */; }; D5E8DD292B47E54800E083E0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E8DD282B47E54800E083E0 /* AppDelegate.swift */; }; D5E8DD312B47E83500E083E0 /* dotlocal-daemon in Copy Daemon */ = {isa = PBXBuildFile; fileRef = D5E8DD2D2B47E7B900E083E0 /* dotlocal-daemon */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -50,9 +80,69 @@ remoteGlobalIDString = D529B1A92B47BF8C00DC288B; remoteInfo = DotLocal; }; + D56116B02B51102F00FEB087 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D529B1A22B47BF8C00DC288B /* Project object */; + proxyType = 1; + remoteGlobalIDString = D59D894C2B4FFC380009270C; + remoteInfo = DotLocalHelperTool; + }; + D5803ED42B5194F100332743 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D529B1A22B47BF8C00DC288B /* Project object */; + proxyType = 1; + remoteGlobalIDString = D5803ECC2B5194CF00332743; + remoteInfo = PropertyListModifier; + }; + D5803ED62B5194FA00332743 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D529B1A22B47BF8C00DC288B /* Project object */; + proxyType = 1; + remoteGlobalIDString = D5803ECC2B5194CF00332743; + remoteInfo = PropertyListModifier; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + D5803ECB2B5194CF00332743 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + D59D894B2B4FFC380009270C /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; + D59D89592B4FFDD30009270C /* Copy Helper */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Contents/Library/LaunchServices; + dstSubfolderSpec = 1; + files = ( + D5803EDE2B519ADA00332743 /* dev.suphon.DotLocal.helper in Copy Helper */, + ); + name = "Copy Helper"; + runOnlyForDeploymentPostprocessing = 0; + }; + D59D895B2B4FFEFB0009270C /* Copy Helper Plist */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Contents/Library/LaunchDaemons; + dstSubfolderSpec = 1; + files = ( + ); + name = "Copy Helper Plist"; + runOnlyForDeploymentPostprocessing = 0; + }; D5E8DD2A2B47E79700E083E0 /* Copy Daemon */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -75,26 +165,21 @@ name = "Copy Client"; runOnlyForDeploymentPostprocessing = 0; }; - D5E8DD412B47F57400E083E0 /* Copy Binaries */ = { + D5E8DD412B47F57400E083E0 /* Copy Nginx */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = bin; dstSubfolderSpec = 7; files = ( - D582E9072B4C5BE20054343B /* nginx in Copy Binaries */, + D582E9072B4C5BE20054343B /* nginx in Copy Nginx */, ); - name = "Copy Binaries"; + name = "Copy Nginx"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - D50378012B482578008F9AA8 /* dot-local.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dot-local.grpc.swift"; sourceTree = ""; }; - D50378022B482578008F9AA8 /* dot-local.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dot-local.pb.swift"; sourceTree = ""; }; - D50378032B482578008F9AA8 /* preferences.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = preferences.pb.swift; sourceTree = ""; }; - D50378082B48708C008F9AA8 /* MappingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MappingListViewModel.swift; sourceTree = ""; }; D503780A2B48718D008F9AA8 /* MappingList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MappingList.swift; sourceTree = ""; }; - D503780C2B487250008F9AA8 /* ProtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtoExtensions.swift; sourceTree = ""; }; D529B1AA2B47BF8C00DC288B /* DotLocal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DotLocal.app; sourceTree = BUILT_PRODUCTS_DIR; }; D529B1AD2B47BF8C00DC288B /* DotLocalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotLocalApp.swift; sourceTree = ""; }; D529B1AF2B47BF8C00DC288B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -106,13 +191,35 @@ D529B1C52B47BF8E00DC288B /* DotLocalUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DotLocalUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D529B1C92B47BF8E00DC288B /* DotLocalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotLocalUITests.swift; sourceTree = ""; }; D529B1CB2B47BF8E00DC288B /* DotLocalUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotLocalUITestsLaunchTests.swift; sourceTree = ""; }; + D56116A92B510CFD00FEB087 /* DaemonManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DaemonManager.swift; sourceTree = ""; }; + D56116B92B51621500FEB087 /* dot-local.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "dot-local.pb.swift"; sourceTree = ""; }; + D56116BA2B51621500FEB087 /* preferences.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = preferences.pb.swift; sourceTree = ""; }; + D56116BB2B51621500FEB087 /* dot-local.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "dot-local.grpc.swift"; sourceTree = ""; }; + D56116C22B51628E00FEB087 /* ProtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtoExtensions.swift; sourceTree = ""; }; + D56116C52B517DC800FEB087 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = ""; }; + D5793C482B50E8D400979DC3 /* HelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperManager.swift; sourceTree = ""; }; + D5793C5F2B5103E300979DC3 /* DaemonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaemonState.swift; sourceTree = ""; }; + D5803ECD2B5194CF00332743 /* PropertyListModifier */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PropertyListModifier; sourceTree = BUILT_PRODUCTS_DIR; }; + D5803ECF2B5194CF00332743 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + D5803EDC2B51990100332743 /* HelperToolMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolMonitor.swift; sourceTree = ""; }; + D5803EE62B51A19500332743 /* Uninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uninstaller.swift; sourceTree = ""; }; + D5803EE82B51A1A200332743 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; D582E9052B4C5BC00054343B /* nginx */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = nginx; sourceTree = ""; }; D582E90A2B4E7BA90054343B /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; D582E90B2B4E7C750054343B /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; + D59D894D2B4FFC380009270C /* dev.suphon.DotLocal.helper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = dev.suphon.DotLocal.helper; sourceTree = BUILT_PRODUCTS_DIR; }; + D59D894F2B4FFC380009270C /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + D59D89602B5048C40009270C /* SharedConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConstants.swift; sourceTree = ""; }; + D59D89632B50506D0009270C /* AppConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppConfig.xcconfig; sourceTree = ""; }; + D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = HelperToolConfig.xcconfig; sourceTree = ""; }; + D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolInfoPropertyList.swift; sourceTree = ""; }; + D59D89712B50572A0009270C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D59D897C2B505E4B0009270C /* CodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeInfo.swift; sourceTree = ""; }; + D59D897F2B505EFE0009270C /* ManageClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageClient.swift; sourceTree = ""; }; + D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolLaunchdPropertyList.swift; sourceTree = ""; }; D5DEA9B22B4888310029BB00 /* AppMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMenu.swift; sourceTree = ""; }; D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D5DEA9BF2B499C2F0029BB00 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; - D5DEA9C12B49A6B60029BB00 /* Sudo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sudo.swift; sourceTree = ""; }; D5DEA9C32B49C0200029BB00 /* ClientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientManager.swift; sourceTree = ""; }; D5E8DD282B47E54800E083E0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D5E8DD2B2B47E7AE00E083E0 /* dotlocal */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = dotlocal; path = go_out/dotlocal; sourceTree = ""; }; @@ -127,7 +234,10 @@ files = ( D5DEA9BC2B4995BC0029BB00 /* Defaults in Frameworks */, D5DEA9B62B49936B0029BB00 /* LaunchAtLogin in Frameworks */, + D59D89792B505C430009270C /* SecureXPC in Frameworks */, + D5803EE22B519E3B00332743 /* Blessed in Frameworks */, D50377F62B481B59008F9AA8 /* GRPC in Frameworks */, + D59D896E2B5055BB0009270C /* EmbeddedPropertyList in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -145,28 +255,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - D50377F72B481F69008F9AA8 /* Model */ = { - isa = PBXGroup; - children = ( - D50378072B482588008F9AA8 /* proto */, - D503780C2B487250008F9AA8 /* ProtoExtensions.swift */, + D5803ECA2B5194CF00332743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ); - path = Model; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - D50378072B482588008F9AA8 /* proto */ = { - isa = PBXGroup; - children = ( - D50378012B482578008F9AA8 /* dot-local.grpc.swift */, - D50378022B482578008F9AA8 /* dot-local.pb.swift */, - D50378032B482578008F9AA8 /* preferences.pb.swift */, + D59D894A2B4FFC380009270C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D5793C5C2B5103CC00979DC3 /* GRPC in Frameworks */, + D5803EE42B519E4600332743 /* Blessed in Frameworks */, + D59D897B2B505C4B0009270C /* SecureXPC in Frameworks */, + D5793C5E2B5103CF00979DC3 /* EmbeddedPropertyList in Frameworks */, ); - path = proto; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ D529B1A12B47BF8C00DC288B = { isa = PBXGroup; children = ( @@ -174,9 +283,13 @@ D582E8FF2B4C5AEE0054343B /* bin */, D5E8DD2D2B47E7B900E083E0 /* dotlocal-daemon */, D5E8DD2B2B47E7AE00E083E0 /* dotlocal */, + D59D895D2B5048420009270C /* Shared */, D529B1AC2B47BF8C00DC288B /* DotLocal */, + D59D894E2B4FFC380009270C /* DotLocalHelperTool */, D529B1C82B47BF8E00DC288B /* DotLocalUITests */, + D5803ECE2B5194CF00332743 /* PropertyListModifier */, D529B1AB2B47BF8C00DC288B /* Products */, + D59D896C2B5055BB0009270C /* Frameworks */, ); sourceTree = ""; }; @@ -186,6 +299,8 @@ D529B1AA2B47BF8C00DC288B /* DotLocal.app */, D529B1BB2B47BF8E00DC288B /* DotLocalTests.xctest */, D529B1C52B47BF8E00DC288B /* DotLocalUITests.xctest */, + D59D894D2B4FFC380009270C /* dev.suphon.DotLocal.helper */, + D5803ECD2B5194CF00332743 /* PropertyListModifier */, ); name = Products; sourceTree = ""; @@ -193,22 +308,23 @@ D529B1AC2B47BF8C00DC288B /* DotLocal */ = { isa = PBXGroup; children = ( - D50377F72B481F69008F9AA8 /* Model */, + D59D89632B50506D0009270C /* AppConfig.xcconfig */, D529B1AD2B47BF8C00DC288B /* DotLocalApp.swift */, D5E8DD282B47E54800E083E0 /* AppDelegate.swift */, D5DEA9B22B4888310029BB00 /* AppMenu.swift */, + D5803EDC2B51990100332743 /* HelperToolMonitor.swift */, D529B1AF2B47BF8C00DC288B /* ContentView.swift */, - D50378082B48708C008F9AA8 /* MappingListViewModel.swift */, D503780A2B48718D008F9AA8 /* MappingList.swift */, - D529B1BE2B47BF8E00DC288B /* DotLocalTests */, - D529B1B12B47BF8E00DC288B /* Assets.xcassets */, - D529B1B62B47BF8E00DC288B /* DotLocal.entitlements */, - D529B1B32B47BF8E00DC288B /* Preview Content */, + D5793C482B50E8D400979DC3 /* HelperManager.swift */, D5E8DD322B47E97F00E083E0 /* DaemonManager.swift */, D5DEA9C32B49C0200029BB00 /* ClientManager.swift */, D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */, D5DEA9BF2B499C2F0029BB00 /* Defaults.swift */, - D5DEA9C12B49A6B60029BB00 /* Sudo.swift */, + D56116C52B517DC800FEB087 /* ViewExtensions.swift */, + D529B1BE2B47BF8E00DC288B /* DotLocalTests */, + D529B1B12B47BF8E00DC288B /* Assets.xcassets */, + D529B1B62B47BF8E00DC288B /* DotLocal.entitlements */, + D529B1B32B47BF8E00DC288B /* Preview Content */, ); path = DotLocal; sourceTree = ""; @@ -238,6 +354,33 @@ path = DotLocalUITests; sourceTree = ""; }; + D56116B62B51621500FEB087 /* Model */ = { + isa = PBXGroup; + children = ( + D56116B72B51621500FEB087 /* proto */, + D56116C22B51628E00FEB087 /* ProtoExtensions.swift */, + ); + path = Model; + sourceTree = ""; + }; + D56116B72B51621500FEB087 /* proto */ = { + isa = PBXGroup; + children = ( + D56116B92B51621500FEB087 /* dot-local.pb.swift */, + D56116BA2B51621500FEB087 /* preferences.pb.swift */, + D56116BB2B51621500FEB087 /* dot-local.grpc.swift */, + ); + path = proto; + sourceTree = ""; + }; + D5803ECE2B5194CF00332743 /* PropertyListModifier */ = { + isa = PBXGroup; + children = ( + D5803ECF2B5194CF00332743 /* main.swift */, + ); + path = PropertyListModifier; + sourceTree = ""; + }; D582E8FF2B4C5AEE0054343B /* bin */ = { isa = PBXGroup; children = ( @@ -263,6 +406,40 @@ path = Config; sourceTree = ""; }; + D59D894E2B4FFC380009270C /* DotLocalHelperTool */ = { + isa = PBXGroup; + children = ( + D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */, + D59D89712B50572A0009270C /* Info.plist */, + D59D894F2B4FFC380009270C /* main.swift */, + D59D897F2B505EFE0009270C /* ManageClient.swift */, + D56116A92B510CFD00FEB087 /* DaemonManager.swift */, + D5803EE62B51A19500332743 /* Uninstaller.swift */, + D5803EE82B51A1A200332743 /* Updater.swift */, + ); + path = DotLocalHelperTool; + sourceTree = ""; + }; + D59D895D2B5048420009270C /* Shared */ = { + isa = PBXGroup; + children = ( + D56116B62B51621500FEB087 /* Model */, + D59D89602B5048C40009270C /* SharedConstants.swift */, + D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */, + D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */, + D59D897C2B505E4B0009270C /* CodeInfo.swift */, + D5793C5F2B5103E300979DC3 /* DaemonState.swift */, + ); + path = Shared; + sourceTree = ""; + }; + D59D896C2B5055BB0009270C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -270,23 +447,31 @@ isa = PBXNativeTarget; buildConfigurationList = D529B1CF2B47BF8E00DC288B /* Build configuration list for PBXNativeTarget "DotLocal" */; buildPhases = ( + D5803ED82B51960100332743 /* Satisfy Job Bless Requirements */, D529B1A62B47BF8C00DC288B /* Sources */, D529B1A72B47BF8C00DC288B /* Frameworks */, D529B1A82B47BF8C00DC288B /* Resources */, - D5E8DD412B47F57400E083E0 /* Copy Binaries */, + D5E8DD412B47F57400E083E0 /* Copy Nginx */, D529B1D82B47C06400DC288B /* Build golang binaries */, D5E8DD2A2B47E79700E083E0 /* Copy Daemon */, D5E8DD2F2B47E82200E083E0 /* Copy Client */, + D59D89592B4FFDD30009270C /* Copy Helper */, + D59D895B2B4FFEFB0009270C /* Copy Helper Plist */, ); buildRules = ( ); dependencies = ( + D5803ED72B5194FA00332743 /* PBXTargetDependency */, + D56116B12B51102F00FEB087 /* PBXTargetDependency */, ); name = DotLocal; packageProductDependencies = ( D50377F52B481B59008F9AA8 /* GRPC */, D5DEA9B52B49936B0029BB00 /* LaunchAtLogin */, D5DEA9BB2B4995BC0029BB00 /* Defaults */, + D59D896D2B5055BB0009270C /* EmbeddedPropertyList */, + D59D89782B505C430009270C /* SecureXPC */, + D5803EE12B519E3B00332743 /* Blessed */, ); productName = DotLocal; productReference = D529B1AA2B47BF8C00DC288B /* DotLocal.app */; @@ -328,6 +513,48 @@ productReference = D529B1C52B47BF8E00DC288B /* DotLocalUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + D5803ECC2B5194CF00332743 /* PropertyListModifier */ = { + isa = PBXNativeTarget; + buildConfigurationList = D5803ED32B5194CF00332743 /* Build configuration list for PBXNativeTarget "PropertyListModifier" */; + buildPhases = ( + D5803EC92B5194CF00332743 /* Sources */, + D5803ECA2B5194CF00332743 /* Frameworks */, + D5803ECB2B5194CF00332743 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PropertyListModifier; + productName = PropertyListModifier; + productReference = D5803ECD2B5194CF00332743 /* PropertyListModifier */; + productType = "com.apple.product-type.tool"; + }; + D59D894C2B4FFC380009270C /* DotLocalHelperTool */ = { + isa = PBXNativeTarget; + buildConfigurationList = D59D89532B4FFC380009270C /* Build configuration list for PBXNativeTarget "DotLocalHelperTool" */; + buildPhases = ( + D59D89722B505A550009270C /* ShellScript */, + D59D89492B4FFC380009270C /* Sources */, + D59D894A2B4FFC380009270C /* Frameworks */, + D59D894B2B4FFC380009270C /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + D5803ED52B5194F100332743 /* PBXTargetDependency */, + ); + name = DotLocalHelperTool; + packageProductDependencies = ( + D59D897A2B505C4B0009270C /* SecureXPC */, + D5793C5B2B5103CC00979DC3 /* GRPC */, + D5793C5D2B5103CF00979DC3 /* EmbeddedPropertyList */, + D5803EE32B519E4600332743 /* Blessed */, + ); + productName = DotLocalHelperTool; + productReference = D59D894D2B4FFC380009270C /* dev.suphon.DotLocal.helper */; + productType = "com.apple.product-type.tool"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -349,6 +576,12 @@ CreatedOnToolsVersion = 15.1; TestTargetID = D529B1A92B47BF8C00DC288B; }; + D5803ECC2B5194CF00332743 = { + CreatedOnToolsVersion = 15.1; + }; + D59D894C2B4FFC380009270C = { + CreatedOnToolsVersion = 15.1; + }; }; }; buildConfigurationList = D529B1A52B47BF8C00DC288B /* Build configuration list for PBXProject "DotLocal" */; @@ -364,14 +597,20 @@ D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */, D5DEA9B42B49936B0029BB00 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, D5DEA9BA2B4995BC0029BB00 /* XCRemoteSwiftPackageReference "Defaults" */, + D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */, + D59D89772B505C430009270C /* XCRemoteSwiftPackageReference "SecureXPC" */, + D5803EE02B519E3B00332743 /* XCRemoteSwiftPackageReference "Blessed" */, + D5803EE52B519E5100332743 /* XCRemoteSwiftPackageReference "Authorized" */, ); productRefGroup = D529B1AB2B47BF8C00DC288B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( D529B1A92B47BF8C00DC288B /* DotLocal */, - D529B1BA2B47BF8E00DC288B /* DotLocalTests */, + D59D894C2B4FFC380009270C /* DotLocalHelperTool */, D529B1C42B47BF8E00DC288B /* DotLocalUITests */, + D529B1BA2B47BF8E00DC288B /* DotLocalTests */, + D5803ECC2B5194CF00332743 /* PropertyListModifier */, ); }; /* End PBXProject section */ @@ -425,7 +664,42 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\nmkdir -p ./go_out\n\nSRC_CHECKSUM=$(find . -type f -name \"*.go\" -or -name \"go.mod\" -or -name \"go.sum\" | xargs cksum)\nEXISTING_CHECKSUM=\"\"\nif [[ -f \"./go_out/src_checksum\" ]]; then\n EXISTING_CHECKSUM=$(cat ./go_out/src_checksum)\nfi\nif [ \"$SRC_CHECKSUM\" == \"$EXISTING_CHECKSUM\" ]; then\n echo \"source hasn't changed. skipping\"\n exit 0\nfi\necho \"$SRC_CHECKSUM\" > ./go_out/src_checksum\n\nif [[ -f \"./.xcode.env\" ]]; then\n source \"./.xcode.env\"\nfi\nif [[ -f \"./.xcode.env.local\" ]]; then\n source \"./.xcode.env.local\"\nfi\n\nGOOS=darwin GOARCH=arm64 $GO_BINARY build -ldflags=\"-w\" -o ./go_out/arm64/dotlocal ./cmd/dotlocal/main.go\nGOOS=darwin GOARCH=arm64 $GO_BINARY build -ldflags=\"-w\" -o ./go_out/arm64/dotlocal-daemon ./cmd/dotlocal-daemon/main.go\nGOOS=darwin GOARCH=amd64 $GO_BINARY build -ldflags=\"-w\" -o ./go_out/amd64/dotlocal ./cmd/dotlocal/main.go\nGOOS=darwin GOARCH=amd64 $GO_BINARY build -ldflags=\"-w\" -o ./go_out/amd64/dotlocal-daemon ./cmd/dotlocal-daemon/main.go\nlipo -create -output ./go_out/dotlocal ./go_out/arm64/dotlocal ./go_out/amd64/dotlocal\nlipo -create -output ./go_out/dotlocal-daemon ./go_out/arm64/dotlocal-daemon ./go_out/amd64/dotlocal-daemon\n"; + shellScript = "set -e\n\nmkdir -p ./go_out\n\nSRC_CHECKSUM=$(find . -type f -name \"*.go\" -or -name \"go.mod\" -or -name \"go.sum\" | xargs cksum)\nSRC_CHECKSUM=\"$SRC_CHECKSUM\\n$(cat $0 | cksum)\"\nEXISTING_CHECKSUM=\"\"\nif [[ -f \"./go_out/src_checksum\" ]]; then\n EXISTING_CHECKSUM=$(cat ./go_out/src_checksum)\nfi\nif [ \"$SRC_CHECKSUM\" == \"$EXISTING_CHECKSUM\" ]; then\n echo \"source hasn't changed. skipping\"\n exit 0\nfi\n\nif [[ -f \"./.xcode.env\" ]]; then\n source \"./.xcode.env\"\nfi\nif [[ -f \"./.xcode.env.local\" ]]; then\n source \"./.xcode.env.local\"\nfi\n\nfunction build_go_target {\n CGO_ENABLED=1 GOOS=darwin GOARCH=$2 $GO_BINARY build -ldflags=\"-w\" -o \"./go_out/$2/$1\" \"./cmd/$1/main.go\"\n}\n\nbuild_go_target dotlocal arm64\nbuild_go_target dotlocal-daemon arm64\nbuild_go_target dotlocal amd64\nbuild_go_target dotlocal-daemon amd64\nlipo -create -output ./go_out/dotlocal ./go_out/arm64/dotlocal ./go_out/amd64/dotlocal\nlipo -create -output ./go_out/dotlocal-daemon ./go_out/arm64/dotlocal-daemon ./go_out/amd64/dotlocal-daemon\n\necho \"$SRC_CHECKSUM\" > ./go_out/src_checksum\n"; + }; + D5803ED82B51960100332743 /* Satisfy Job Bless Requirements */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Satisfy Job Bless Requirements"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$BUILT_PRODUCTS_DIR/PropertyListModifier satisfy-job-bless-requirements\n"; + }; + D59D89722B505A550009270C /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$BUILT_PRODUCTS_DIR/PropertyListModifier satisfy-job-bless-requirements specify-mach-services\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -434,20 +708,26 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D5793C602B5103E300979DC3 /* DaemonState.swift in Sources */, + D56116C62B517DC800FEB087 /* ViewExtensions.swift in Sources */, + D56116BE2B51621500FEB087 /* preferences.pb.swift in Sources */, + D59D897D2B505E4B0009270C /* CodeInfo.swift in Sources */, D5E8DD292B47E54800E083E0 /* AppDelegate.swift in Sources */, + D56116C32B51628E00FEB087 /* ProtoExtensions.swift in Sources */, D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */, - D50378092B48708C008F9AA8 /* MappingListViewModel.swift in Sources */, + D59D89862B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, + D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */, + D5803EDD2B51990100332743 /* HelperToolMonitor.swift in Sources */, D5E8DD332B47E97F00E083E0 /* DaemonManager.swift in Sources */, + D59D89612B5048C40009270C /* SharedConstants.swift in Sources */, D5DEA9B32B4888310029BB00 /* AppMenu.swift in Sources */, + D56116C02B51621500FEB087 /* dot-local.grpc.swift in Sources */, D529B1B02B47BF8C00DC288B /* ContentView.swift in Sources */, - D503780D2B487250008F9AA8 /* ProtoExtensions.swift in Sources */, + D59D89692B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */, D5DEA9BE2B4995DD0029BB00 /* SettingsView.swift in Sources */, - D50378042B482578008F9AA8 /* dot-local.pb.swift in Sources */, - D50378052B482578008F9AA8 /* dot-local.grpc.swift in Sources */, D503780B2B48718D008F9AA8 /* MappingList.swift in Sources */, D529B1AE2B47BF8C00DC288B /* DotLocalApp.swift in Sources */, - D5DEA9C22B49A6B60029BB00 /* Sudo.swift in Sources */, - D50378062B482578008F9AA8 /* preferences.pb.swift in Sources */, + D56116BC2B51621500FEB087 /* dot-local.pb.swift in Sources */, D5DEA9C42B49C0200029BB00 /* ClientManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -469,6 +749,36 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D5803EC92B5194CF00332743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5803ED02B5194CF00332743 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D59D89492B4FFC380009270C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5793C612B5103E300979DC3 /* DaemonState.swift in Sources */, + D59D897E2B505E4B0009270C /* CodeInfo.swift in Sources */, + D59D89622B5048C40009270C /* SharedConstants.swift in Sources */, + D56116C12B51621500FEB087 /* dot-local.grpc.swift in Sources */, + D56116BF2B51621500FEB087 /* preferences.pb.swift in Sources */, + D59D89812B505EFE0009270C /* ManageClient.swift in Sources */, + D59D89872B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, + D56116C72B517DC800FEB087 /* ViewExtensions.swift in Sources */, + D56116C42B51628E00FEB087 /* ProtoExtensions.swift in Sources */, + D56116AA2B510CFD00FEB087 /* DaemonManager.swift in Sources */, + D59D896A2B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */, + D5803EE72B51A19500332743 /* Uninstaller.swift in Sources */, + D56116BD2B51621500FEB087 /* dot-local.pb.swift in Sources */, + D59D89502B4FFC380009270C /* main.swift in Sources */, + D5803EE92B51A1A200332743 /* Updater.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -482,12 +792,26 @@ target = D529B1A92B47BF8C00DC288B /* DotLocal */; targetProxy = D529B1C62B47BF8E00DC288B /* PBXContainerItemProxy */; }; + D56116B12B51102F00FEB087 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D59D894C2B4FFC380009270C /* DotLocalHelperTool */; + targetProxy = D56116B02B51102F00FEB087 /* PBXContainerItemProxy */; + }; + D5803ED52B5194F100332743 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D5803ECC2B5194CF00332743 /* PropertyListModifier */; + targetProxy = D5803ED42B5194F100332743 /* PBXContainerItemProxy */; + }; + D5803ED72B5194FA00332743 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D5803ECC2B5194CF00332743 /* PropertyListModifier */; + targetProxy = D5803ED62B5194FA00332743 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ D529B1CD2B47BF8E00DC288B /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D582E90A2B4E7BA90054343B /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -551,7 +875,6 @@ }; D529B1CE2B47BF8E00DC288B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D582E90A2B4E7BA90054343B /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -607,6 +930,7 @@ }; D529B1D02B47BF8E00DC288B /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D59D89632B50506D0009270C /* AppConfig.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -627,7 +951,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = dev.suphon.DotLocal; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -636,6 +959,7 @@ }; D529B1D12B47BF8E00DC288B /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D59D89632B50506D0009270C /* AppConfig.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -656,7 +980,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = dev.suphon.DotLocal; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -735,6 +1058,60 @@ }; name = Release; }; + D5803ED12B5194CF00332743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + MACOSX_DEPLOYMENT_TARGET = 13.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D5803ED22B5194CF00332743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + MACOSX_DEPLOYMENT_TARGET = 13.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + D59D89512B4FFC380009270C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 2HCNNF2TB5; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "$(SRCROOT)/DotLocalHelperTool/Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = "$(MARKETING_VERSION)"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_NAME = "$(HELPER_TOOL_BUNDLE_IDENTIFIER)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D59D89522B4FFC380009270C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 2HCNNF2TB5; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "$(SRCROOT)/DotLocalHelperTool/Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = "$(MARKETING_VERSION)"; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_NAME = "$(HELPER_TOOL_BUNDLE_IDENTIFIER)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -774,6 +1151,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D5803ED32B5194CF00332743 /* Build configuration list for PBXNativeTarget "PropertyListModifier" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D5803ED12B5194CF00332743 /* Debug */, + D5803ED22B5194CF00332743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D59D89532B4FFC380009270C /* Build configuration list for PBXNativeTarget "DotLocalHelperTool" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D59D89512B4FFC380009270C /* Debug */, + D59D89522B4FFC380009270C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -785,6 +1180,38 @@ minimumVersion = 1.21.0; }; }; + D5803EE02B519E3B00332743 /* XCRemoteSwiftPackageReference "Blessed" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/trilemma-dev/Blessed"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.6.0; + }; + }; + D5803EE52B519E5100332743 /* XCRemoteSwiftPackageReference "Authorized" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/trilemma-dev/Authorized"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/trilemma-dev/EmbeddedPropertyList.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.2; + }; + }; + D59D89772B505C430009270C /* XCRemoteSwiftPackageReference "SecureXPC" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/trilemma-dev/SecureXPC"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.8.0; + }; + }; D5DEA9B42B49936B0029BB00 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; @@ -809,6 +1236,41 @@ package = D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */; productName = GRPC; }; + D5793C5B2B5103CC00979DC3 /* GRPC */ = { + isa = XCSwiftPackageProductDependency; + package = D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = GRPC; + }; + D5793C5D2B5103CF00979DC3 /* EmbeddedPropertyList */ = { + isa = XCSwiftPackageProductDependency; + package = D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; + productName = EmbeddedPropertyList; + }; + D5803EE12B519E3B00332743 /* Blessed */ = { + isa = XCSwiftPackageProductDependency; + package = D5803EE02B519E3B00332743 /* XCRemoteSwiftPackageReference "Blessed" */; + productName = Blessed; + }; + D5803EE32B519E4600332743 /* Blessed */ = { + isa = XCSwiftPackageProductDependency; + package = D5803EE02B519E3B00332743 /* XCRemoteSwiftPackageReference "Blessed" */; + productName = Blessed; + }; + D59D896D2B5055BB0009270C /* EmbeddedPropertyList */ = { + isa = XCSwiftPackageProductDependency; + package = D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; + productName = EmbeddedPropertyList; + }; + D59D89782B505C430009270C /* SecureXPC */ = { + isa = XCSwiftPackageProductDependency; + package = D59D89772B505C430009270C /* XCRemoteSwiftPackageReference "SecureXPC" */; + productName = SecureXPC; + }; + D59D897A2B505C4B0009270C /* SecureXPC */ = { + isa = XCSwiftPackageProductDependency; + package = D59D89772B505C430009270C /* XCRemoteSwiftPackageReference "SecureXPC" */; + productName = SecureXPC; + }; D5DEA9B52B49936B0029BB00 /* LaunchAtLogin */ = { isa = XCSwiftPackageProductDependency; package = D5DEA9B42B49936B0029BB00 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; diff --git a/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ea42eec..f80534b 100644 --- a/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,23 @@ { "pins" : [ + { + "identity" : "authorized", + "kind" : "remoteSourceControl", + "location" : "https://github.com/trilemma-dev/Authorized.git", + "state" : { + "revision" : "e490b9d3f4a0e8b17a8b39b5a9750b8e0be7548a", + "version" : "1.0.0" + } + }, + { + "identity" : "blessed", + "kind" : "remoteSourceControl", + "location" : "https://github.com/trilemma-dev/Blessed", + "state" : { + "revision" : "e7c730ea4bcd2df7b61f022dbd38c5cdc2c875de", + "version" : "0.6.0" + } + }, { "identity" : "defaults", "kind" : "remoteSourceControl", @@ -9,6 +27,15 @@ "revision" : "d8a9f5105607c85b544558e7f5b51d6c360ba88b" } }, + { + "identity" : "embeddedpropertylist", + "kind" : "remoteSourceControl", + "location" : "https://github.com/trilemma-dev/EmbeddedPropertyList.git", + "state" : { + "revision" : "21bd832e28a9a66ecdb7b4c21910bb0487a22fe5", + "version" : "2.0.2" + } + }, { "identity" : "grpc-swift", "kind" : "remoteSourceControl", @@ -27,6 +54,24 @@ "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc" } }, + { + "identity" : "required", + "kind" : "remoteSourceControl", + "location" : "https://github.com/trilemma-dev/Required.git", + "state" : { + "revision" : "82a4fbd388346ca40b1bbe815014dc45a75d503c", + "version" : "0.1.1" + } + }, + { + "identity" : "securexpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/trilemma-dev/SecureXPC", + "state" : { + "revision" : "d6e439e2b805de8be9b584fff97cf2f6a839a656", + "version" : "0.8.0" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", diff --git a/DotLocal/AppConfig.xcconfig b/DotLocal/AppConfig.xcconfig new file mode 100644 index 0000000..3c8635a --- /dev/null +++ b/DotLocal/AppConfig.xcconfig @@ -0,0 +1,18 @@ +// +// AppConfig.xcconfig +// DotLocal +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Config/Config.xcconfig" + +TARGET_DIRECTORY = DotLocal + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER) +SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP + +INFOPLIST_FILE = $(TARGET_DIRECTORY)/Info.plist diff --git a/DotLocal/AppDelegate.swift b/DotLocal/AppDelegate.swift index 3ae59ba..ceb8bc8 100644 --- a/DotLocal/AppDelegate.swift +++ b/DotLocal/AppDelegate.swift @@ -8,10 +8,11 @@ import Foundation import AppKit import Defaults +import SecureXPC class AppDelegate: NSObject, NSApplicationDelegate { - func applicationDidFinishLaunching(_ notification: Notification) { - DaemonManager.shared.start() + override init() { + _ = HelperManager.shared ClientManager.shared.checkInstalled() } @@ -27,8 +28,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { return true } - func applicationWillTerminate(_ notification: Notification) { - DaemonManager.shared.stop() - DaemonManager.shared.wait() + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + print("applicationShouldTerminate called, stopping daemon and helper") + if HelperManager.shared.installationStatus.isReady { + Task { + await DaemonManager.shared.stop() + try? await HelperManager.shared.xpcClient.send(to: SharedConstants.exitRoute) + NSApplication.shared.terminate(nil) + } + return .terminateLater + } else { + return .terminateNow + } } } diff --git a/DotLocal/AppMenu.swift b/DotLocal/AppMenu.swift index 57a034a..77abab1 100644 --- a/DotLocal/AppMenu.swift +++ b/DotLocal/AppMenu.swift @@ -17,11 +17,11 @@ struct AppMenu: View { switch daemonManager.state { case .stopped: Button("DotLocal is not running") {}.disabled(true) - case .starting: + case .starting, .unknown: Button("DotLocal is starting") {}.disabled(true) - case .started: + case .started(let mappings): Section("Routes") { - MappingListMenu() + MappingListMenu(mappings: mappings) } } Divider() @@ -39,14 +39,14 @@ struct AppMenu: View { } struct MappingListMenu: View { - @StateObject var vm = MappingListViewModel() + var mappings: [Mapping] @Environment(\.openURL) var openURL var body: some View { - if vm.mappings.isEmpty { + if mappings.isEmpty { Button("No Routes", action: {}).disabled(true) } else { - ForEach(vm.mappings) { mapping in + ForEach(mappings) { mapping in let url = URL(string: "http://\(mapping.host)\(mapping.pathPrefix)")! Button(action: { openURL(url) }, label: { Text(getLabel(mapping: mapping)) diff --git a/DotLocal/ClientManager.swift b/DotLocal/ClientManager.swift index fb5f957..970122c 100644 --- a/DotLocal/ClientManager.swift +++ b/DotLocal/ClientManager.swift @@ -6,24 +6,32 @@ // import Foundation +import SecureXPC class ClientManager: ObservableObject { static let shared = ClientManager() @Published var installed = false - private let clientUrl = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/bin/dotlocal") private let target = "/usr/local/bin/dotlocal" private init() {} func installCli() async { - _ = await Sudo.run(path: clientUrl.path(percentEncoded: false), arguments: ["install"]) - checkInstalled() + do { + try await HelperManager.shared.xpcClient.sendMessage(Bundle.main.bundleURL, to: SharedConstants.installClientRoute) + checkInstalled() + } catch { + print("error installing cli: \(error)") + } } func uninstallCli() async { - _ = await Sudo.run(path: clientUrl.path(percentEncoded: false), arguments: ["uninstall"]) - checkInstalled() + do { + try await HelperManager.shared.xpcClient.send(to: SharedConstants.uninstallClientRoute) + checkInstalled() + } catch { + print("error uninstalling cli: \(error)") + } } func checkInstalled() { diff --git a/DotLocal/ContentView.swift b/DotLocal/ContentView.swift index 2b5b4b6..2e91d42 100644 --- a/DotLocal/ContentView.swift +++ b/DotLocal/ContentView.swift @@ -6,25 +6,43 @@ // import SwiftUI +import ServiceManagement +import Blessed +import Authorized +import SecureXPC struct ContentView: View { @StateObject var daemonManager = DaemonManager.shared + @StateObject var helperManager = HelperManager.shared var body: some View { + let status = helperManager.installationStatus VStack { - switch daemonManager.state { - case .stopped: - Text("DotLocal is not running") - case .starting: - ProgressView() - case .started: - MappingList() + if status.isReady { + VStack { + switch daemonManager.state { + case .stopped: + Text("DotLocal is not running") + case .starting, .unknown: + ProgressView() + case .started: + MappingList() + } + }.toolbar() { + StartStopButton(state: daemonManager.state, onStart: { + Task { + await daemonManager.start() + } + }, onStop: { + Task { + await daemonManager.stop() + } + }) + } + } else { + RequiresHelperView() } } - .frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, maxHeight: .infinity) - .toolbar() { - StartStopButton(state: daemonManager.state, onStart: { daemonManager.start() }, onStop: { daemonManager.stop() }) - } } } @@ -39,7 +57,7 @@ struct StartStopButton: View { Button(action: onStart) { Label("Start", systemImage: "play.fill") } - case .starting: + case .starting, .unknown: ProgressView().controlSize(.small) case .started: Button(action: onStop) { @@ -49,6 +67,41 @@ struct StartStopButton: View { } } +struct RequiresHelperView: View { + @State private var didError = false + @State private var errorMessage = "" + + var body: some View { + VStack(spacing: 8) { + Text("Helper Not Installed").font(.title).fontWeight(.bold) + Text("Please install the helper in order to use DotLocal") + Button(action: { + do { + try PrivilegedHelperManager.shared + .authorizeAndBless(message: nil) + } catch AuthorizationError.canceled { + // No user feedback needed, user canceled + } catch { + errorMessage = error.localizedDescription + didError = true + } + }, label: { + Text("Install Helper") + }) + } + .foregroundStyle(.secondary) + .alert( + "Install failed", + isPresented: $didError, + presenting: errorMessage + ) { _ in + Button("OK") {} + } message: { message in + Text(message) + } + } +} + #Preview { ContentView() } diff --git a/DotLocal/DaemonManager.swift b/DotLocal/DaemonManager.swift index 87b617a..57f44c9 100644 --- a/DotLocal/DaemonManager.swift +++ b/DotLocal/DaemonManager.swift @@ -13,106 +13,53 @@ import Combine class DaemonManager: ObservableObject { static let shared = DaemonManager() - private let binUrl = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/bin") - private let daemonUrl = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/dotlocal-daemon") - @Published var state: DaemonState = .stopped - private var task: Process? = nil - private(set) var apiClient: DotLocalAsyncClient? = nil - private var group: EventLoopGroup? = nil - private let _updates = PassthroughSubject() + @Published var state: DaemonState = .unknown + @Published var mappings: [Mapping] = [] + + private var subscribing = false private init() { } - func start() { - if state != .stopped { - return - } - state = .starting - - let binPath = binUrl.path(percentEncoded: false) - let launchPath = daemonUrl.path(percentEncoded: false) - - let task = Process() - var environment = ProcessInfo.processInfo.environment - environment["PATH"] = binPath - task.environment = environment - task.launchPath = launchPath - task.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".dotlocal") - - let outputPipe = Pipe() - task.standardError = outputPipe - task.launch() - - let handle = outputPipe.fileHandleForReading - let token = NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading, queue: nil) { _ in - let chunk = String(decoding: handle.availableData, as: UTF8.self) - print(chunk, terminator: "") - if chunk.contains("API server listening") { - DispatchQueue.main.async { - self.onStart() - } - } else if chunk.contains("Updated mappings") { - print("sending update") - self._updates.send() + func start() async { + do { + print("starting daemon") + try await HelperManager.shared.xpcClient.sendMessage(Bundle.main.bundleURL, to: SharedConstants.startDaemonRoute) + print("successfully requested start") + Task { + await subscribeDaemonState() } - handle.waitForDataInBackgroundAndNotify() + } catch { + print("error starting daemon: \(error)") } - handle.waitForDataInBackgroundAndNotify() - - DispatchQueue.global().async { - task.waitUntilExit() - DispatchQueue.main.async { - NotificationCenter.default.removeObserver(token) - self.onStop() - } - } - self.task = task - } - - private func onStart() { - let socketPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".dotlocal/api.sock") - let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) - self.group = group - // TODO: try catch - let channel = try! GRPCChannelPool.with( - target: .unixDomainSocket(socketPath.path(percentEncoded: false)), - transportSecurity: .plaintext, - eventLoopGroup: group - ) - let apiClient = DotLocalAsyncClient(channel: channel) - self.apiClient = apiClient - - state = .started - } - - private func onStop() { - task = nil - apiClient = nil - state = .stopped } - func stop() { - guard let task = task else { - return + func stop() async { + do { + print("stopping daemon") + try await HelperManager.shared.xpcClient.send(to: SharedConstants.stopDaemonRoute) + print("successfully requested stop") + } catch { + print("error stopping daemon: \(error)") } - task.terminate() } - func wait() { - guard let task = task else { + private func subscribeDaemonState() async { + if subscribing { return } - task.waitUntilExit() - } - - func updates() -> AnyPublisher { - return _updates.prepend(()).eraseToAnyPublisher() + subscribing = true + do { + for try await state in HelperManager.shared.xpcClient.send(to: SharedConstants.daemonStateRoute) { + DispatchQueue.main.async { + self.state = state + if case .started(let mappings) = state { + self.mappings = mappings + } + } + } + } catch { + print("error during state subscription: \(error)") + } } } - -enum DaemonState { - case stopped - case starting - case started -} diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift new file mode 100644 index 0000000..8eff568 --- /dev/null +++ b/DotLocal/HelperManager.swift @@ -0,0 +1,57 @@ +// +// HelperManager.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation +import ServiceManagement +import SecureXPC + +class HelperManager: ObservableObject { + static let shared = HelperManager() + + private let monitor = HelperToolMonitor(constants: SharedConstants.shared) + @Published var installationStatus: HelperToolMonitor.InstallationStatus + private var started = false + + let xpcClient = XPCClient.forMachService(named: SharedConstants.shared.machServiceName) + + private init() { + installationStatus = monitor.determineStatus() + monitor.start { status in + DispatchQueue.main.async { + self.updateStatus(status: status) + } + } + Task { + if installationStatus.isReady { + try await updateHelper() + } + } + } + + private func updateHelper() async throws { + do { + print("updating helper") + try await xpcClient.sendMessage(SharedConstants.shared.bundledLocation, to: SharedConstants.updateRoute) + } catch XPCError.connectionInterrupted { + print("update success") + return + } catch { + print("update error: \(error)") + throw error + } + } + + private func updateStatus(status: HelperToolMonitor.InstallationStatus) { + installationStatus = status + if status.isReady, !started { + started = true + Task { + await DaemonManager.shared.start() + } + } + } +} diff --git a/DotLocal/HelperToolMonitor.swift b/DotLocal/HelperToolMonitor.swift new file mode 100644 index 0000000..62658ff --- /dev/null +++ b/DotLocal/HelperToolMonitor.swift @@ -0,0 +1,134 @@ +// +// HelperToolMonitor.swift +// SwiftAuthorizationSample +// +// Created by Josh Kaplan on 2021-10-23 +// + +import Foundation +import EmbeddedPropertyList + +/// Monitors the on disk location of the helper tool and its launchd property list. +/// +/// Whenever those files change, the helper tool's embedded info property list is read and the launchd status is queried (via the public interface to launchctl). This +/// means this monitor has a limitation that if *only* the launchd registration changes then this monitor will not automatically pick up this changed. However, if +/// `determineStatus()` is called it will always reflect the latest state including querying launchd status. +class HelperToolMonitor { + /// Encapsulates the installation status at approximately a moment in time. + /// + /// The individual properties of this struct can't be queried all at once, so it is possible for this to reflect a state that never truly existed simultaneously. + struct InstallationStatus { + + /// Status of the helper tool executable as exists on disk. + enum HelperToolExecutable { + /// The helper tool exists in its expected location. + /// + /// Associated value is the helper tool's bundle version. + case exists(BundleVersion) + /// No helper tool was found. + case missing + } + + /// The helper tool is registered with launchd (according to launchctl). + let registeredWithLaunchd: Bool + /// The property list used by launchd exists on disk. + let registrationPropertyListExists: Bool + /// Whether an on disk representation of the helper tool exists in its "blessed" location. + let helperToolExecutable: HelperToolExecutable + + var isReady: Bool { + get { + if registeredWithLaunchd, registrationPropertyListExists, case .exists(_) = helperToolExecutable { + return true + } else { + return false + } + } + } + } + + /// Directories containing installed helper tools and their registration property lists. + private let monitoredDirectories: [URL] + /// Mapping of monitored directories to corresponding dispatch sources. + private var dispatchSources = [URL : DispatchSourceFileSystemObject]() + /// Queue to receive callbacks on. + private let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent) + /// Name of the privileged executable being monitored + private let constants: SharedConstants + + /// Creates the monitor. + /// - Parameter constants: Constants defining needed file paths. + init(constants: SharedConstants) { + self.constants = constants + self.monitoredDirectories = [constants.blessedLocation.deletingLastPathComponent(), + constants.blessedPropertyListLocation.deletingLastPathComponent()] + } + + /// Starts the monitoring process. + /// + /// If it's already been started, this will have no effect. This function is not thread safe. + /// - Parameter changeOccurred: Called when the helper tool or registration property list file is created, deleted, or modified. + func start(changeOccurred: @escaping (InstallationStatus) -> Void) { + if dispatchSources.isEmpty { + for monitoredDirectory in monitoredDirectories { + let fileDescriptor = open((monitoredDirectory as NSURL).fileSystemRepresentation, O_EVTONLY) + let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, + eventMask: .write, + queue: directoryMonitorQueue) + dispatchSources[monitoredDirectory] = dispatchSource + dispatchSource.setEventHandler { + changeOccurred(self.determineStatus()) + } + dispatchSource.setCancelHandler { + close(fileDescriptor) + self.dispatchSources.removeValue(forKey: monitoredDirectory) + } + dispatchSource.resume() + } + } + } + + /// Stops the monitoring process. + /// + /// If the process wa never started, this will have no effect. This function is not thread safe. + func stop() { + for source in dispatchSources.values { + source.cancel() + } + } + + /// Determines the installation status of the helper tool + /// - Returns: The status of the helper tool installation. + func determineStatus() -> InstallationStatus { + // Sleep for 50ms because on disk file changes, which triggers this call, can occur before launchctl knows about + // the (de)registration + Thread.sleep(forTimeInterval: 0.05) + + // Registered with launchd + let process = Process() + process.launchPath = "/bin/launchctl" + process.arguments = ["print", "system/\(constants.helperToolLabel)"] + process.qualityOfService = QualityOfService.userInitiated + process.standardOutput = nil + process.standardError = nil + process.launch() + process.waitUntilExit() + let registeredWithLaunchd = (process.terminationStatus == 0) + + // Registration property list exists on disk + let registrationPropertyListExists = FileManager.default + .fileExists(atPath: constants.blessedPropertyListLocation.path) + + let helperToolExecutable: InstallationStatus.HelperToolExecutable + do { + let infoPropertyList = try HelperToolInfoPropertyList(from: constants.blessedLocation) + helperToolExecutable = .exists(infoPropertyList.version) + } catch { + helperToolExecutable = .missing + } + + return InstallationStatus(registeredWithLaunchd: registeredWithLaunchd, + registrationPropertyListExists: registrationPropertyListExists, + helperToolExecutable: helperToolExecutable) + } +} diff --git a/DotLocal/Info.plist b/DotLocal/Info.plist new file mode 100644 index 0000000..a9e5b5f --- /dev/null +++ b/DotLocal/Info.plist @@ -0,0 +1,11 @@ + + + + + SMPrivilegedExecutables + + dev.suphon.DotLocal.helper + anchor apple generic and identifier "dev.suphon.DotLocal.helper" and certificate leaf[subject.OU] = "2HCNNF2TB5" + + + diff --git a/DotLocal/MappingList.swift b/DotLocal/MappingList.swift index e95e0dc..15f6bd2 100644 --- a/DotLocal/MappingList.swift +++ b/DotLocal/MappingList.swift @@ -9,10 +9,11 @@ import SwiftUI struct MappingList: View { @StateObject var clientManager = ClientManager.shared - @StateObject var vm = MappingListViewModel() + @StateObject var daemonManager = DaemonManager.shared var body: some View { - List(vm.mappings) { mapping in + let mappings = daemonManager.mappings + List(mappings) { mapping in HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text("\(mapping.host)\(mapping.pathPrefix)") @@ -27,10 +28,15 @@ struct MappingList: View { } .padding(.vertical, 4) } + .if(!mappings.isEmpty) { + if mappings.count > 1 { + $0.navigationSubtitle("\(mappings.count) routes") + } else { + $0.navigationSubtitle("1 route") + } + } .overlay { - if vm.loading { - ProgressView() - } else if vm.mappings.isEmpty { + if mappings.isEmpty { if #available(macOS 14.0, *) { ContentUnavailableView { Label("No Routes", systemImage: "arrow.triangle.swap") @@ -49,6 +55,13 @@ struct MappingList: View { } } + private func getMappings(state: DaemonState) -> [Mapping] { + if case .started(let mappings) = state { + return mappings + } + return [] + } + @ViewBuilder private func hintView() -> some View { VStack(spacing: 4) { diff --git a/DotLocal/MappingListViewModel.swift b/DotLocal/MappingListViewModel.swift deleted file mode 100644 index 23214bd..0000000 --- a/DotLocal/MappingListViewModel.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// MappingListViewModel.swift -// DotLocal -// -// Created by Suphon Thanakornpakapong on 6/1/2567 BE. -// - -import Foundation -import Combine - -@MainActor class MappingListViewModel: ObservableObject { - @Published var loading = true - @Published var mappings = [Mapping]() - - private var subscriptions = Set() - - init() { - DaemonManager.shared.updates() - .flatMap { _ in - Future<[Mapping], Never> { promise in - Task { - guard let apiClient = DaemonManager.shared.apiClient else { - promise(.success([])) - return - } - let res = try await apiClient.listMappings(.with({_ in})) - promise(.success(res.mappings.sorted())) - } - } - }.sink { mappings in - self.loading = false - self.mappings = mappings - }.store(in: &subscriptions) - } -} diff --git a/DotLocal/Model/ProtoExtensions.swift b/DotLocal/Model/ProtoExtensions.swift deleted file mode 100644 index 1a7947b..0000000 --- a/DotLocal/Model/ProtoExtensions.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ProtoExtensions.swift -// DotLocal -// -// Created by Suphon Thanakornpakapong on 6/1/2567 BE. -// - -import Foundation - -extension Mapping: Identifiable {} - -extension Mapping: Comparable { - public static func < (lhs: Mapping, rhs: Mapping) -> Bool { - let lhsTitle = "\(lhs.host)\(lhs.pathPrefix)" - let rhsTitle = "\(rhs.host)\(rhs.pathPrefix)" - return lhsTitle < rhsTitle - } -} diff --git a/DotLocal/SettingsView.swift b/DotLocal/SettingsView.swift index 5e4d219..c5aea59 100644 --- a/DotLocal/SettingsView.swift +++ b/DotLocal/SettingsView.swift @@ -9,6 +9,7 @@ import SwiftUI import LaunchAtLogin import Defaults import Foundation +import SecureXPC struct GeneralSettingsView: View { @Default(.showInMenuBar) var showInMenuBar diff --git a/DotLocal/Sudo.swift b/DotLocal/Sudo.swift deleted file mode 100644 index 28a435a..0000000 --- a/DotLocal/Sudo.swift +++ /dev/null @@ -1,86 +0,0 @@ -// https://stackoverflow.com/a/69788418 - -import Foundation -import Security - -public struct Sudo { - - private typealias AuthorizationExecuteWithPrivilegesImpl = @convention(c) ( - AuthorizationRef, - UnsafePointer, // path - AuthorizationFlags, - UnsafePointer?>, // args - UnsafeMutablePointer>? - ) -> OSStatus - - /// This wraps the deprecated AuthorizationExecuteWithPrivileges - /// and makes it accessible by Swift - /// - /// - Parameters: - /// - path: The executable path - /// - arguments: The executable arguments - /// - Returns: `errAuthorizationSuccess` or an error code - public static func run(path: String, arguments: [String]) async -> (Bool, String) { - var authRef: AuthorizationRef! - var status = AuthorizationCreate(nil, nil, [], &authRef) - - guard status == errAuthorizationSuccess else { return (false, "") } - defer { AuthorizationFree(authRef, [.destroyRights]) } - - var item = kAuthorizationRightExecute.withCString { name in - AuthorizationItem(name: name, valueLength: 0, value: nil, flags: 0) - } - var rights = withUnsafeMutablePointer(to: &item) { ptr in - AuthorizationRights(count: 1, items: ptr) - } - - status = AuthorizationCopyRights(authRef, &rights, nil, [.interactionAllowed, .preAuthorize, .extendRights], nil) - - guard status == errAuthorizationSuccess else { return (false, "") } - - let (osStatus, stdout) = await executeWithPrivileges(authorization: authRef, path: path, arguments: arguments) - - return (osStatus == errAuthorizationSuccess, stdout) - } - - private static func executeWithPrivileges(authorization: AuthorizationRef, - path: String, - arguments: [String]) async -> (OSStatus, String) { - let RTLD_DEFAULT = dlopen(nil, RTLD_NOW) - guard let funcPtr = dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges") else { return (-1, "") } - let args = arguments.map { strdup($0) } - defer { args.forEach { free($0) }} - let impl = unsafeBitCast(funcPtr, to: AuthorizationExecuteWithPrivilegesImpl.self) - var communicationsPipe = UnsafeMutablePointer.allocate(capacity: 1) - let osStatus = impl(authorization, path, [], args, &communicationsPipe) - let _file = communicationsPipe.pointee - return await withCheckedContinuation { continuation in - DispatchQueue.global().async { - var fileContent = "" - defer { - continuation.resume(returning: (osStatus, fileContent)) - } - - var file = _file - guard file._read != nil else { return } - - let bufferSize = 1024 - var buffer = [UInt8](repeating: 0, count: bufferSize) - - // Read data from the file - var bytesRead = fread(&buffer, 1, bufferSize, &file) - - var content = Data(buffer.prefix(bytesRead)) - - // Continue reading until the end of the file - while bytesRead > 0 { - bytesRead = fread(&buffer, 1, bufferSize, &file) - content.append(contentsOf: buffer.prefix(bytesRead)) - } - - // Convert the data to a string (adjust the encoding as needed) - fileContent = String(data: content, encoding: .utf8) ?? "" - } - } - } -} diff --git a/DotLocal/ViewExtensions.swift b/DotLocal/ViewExtensions.swift new file mode 100644 index 0000000..c0eb40d --- /dev/null +++ b/DotLocal/ViewExtensions.swift @@ -0,0 +1,24 @@ +// +// ViewExtensions.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation +import SwiftUI + +extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} diff --git a/DotLocalHelperTool/DaemonManager.swift b/DotLocalHelperTool/DaemonManager.swift new file mode 100644 index 0000000..329c382 --- /dev/null +++ b/DotLocalHelperTool/DaemonManager.swift @@ -0,0 +1,177 @@ +// +// DaemonManager.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation +import GRPC +import NIO +import Combine +import SecureXPC + +class DaemonManager { + static let shared = DaemonManager() + + private let runDirectory = URL.init(filePath: "/var/run/dotlocal") + + var internalState: DaemonStateInternal = .stopped + private var task: Process? = nil + private(set) var apiClient: DotLocalAsyncClient? = nil + private var group: EventLoopGroup? = nil + private let _updates = PassthroughSubject() + + private let _internalStates = PassthroughSubject() + private let states = CurrentValueSubject(.stopped) + + private var subscriptions = Set() + + private init() { + _internalStates + .flatMap { state in + Future { promise in + Task { + promise(.success(await DaemonManager.mapState(state))) + } + } + } + .subscribe(states) + .store(in: &subscriptions) + } + + func start(bundleURL: URL) async throws { + let binURL = bundleURL.appending(path: "Contents/Resources/bin") + let daemonURL = bundleURL.appending(path: "Contents/Resources/dotlocal-daemon") + + NSLog("received start") + NSLog("daemonURL: \(daemonURL)") + + guard try CodeInfo.doesPublicKeyMatch(forExecutable: daemonURL) else { + NSLog("start daemon failed: security requirements not met") + return + } + + NSLog("security requirements passed") + if internalState != .stopped { + return + } + setState(.starting) + + let binPath = binURL.path(percentEncoded: false) + let launchPath = daemonURL.path(percentEncoded: false) + + try! FileManager.default.createDirectory(at: runDirectory, withIntermediateDirectories: true) + + let task = Process() + var environment = ProcessInfo.processInfo.environment + environment["PATH"] = binPath + task.environment = environment + task.launchPath = launchPath + task.currentDirectoryURL = runDirectory + + task.standardOutput = FileHandle.standardOutput + let outputPipe = Pipe() + task.standardError = outputPipe + outputPipe.fileHandleForReading.readabilityHandler = { handle in + let chunk = String(decoding: handle.availableData, as: UTF8.self) + print(chunk, terminator: "") + if chunk.contains("API server listening") { + DispatchQueue.main.async { + self.onStart() + } + } else if chunk.contains("Updated mappings") { + NSLog("sending update") + self.setState(.started) + } + } + + task.terminationHandler = { _ in + self.onStop() + } + + task.launch() + NSLog("launched") + + self.task = task + } + + private func onStart() { + let socketPath = runDirectory.appending(path: "api.sock") + let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) + self.group = group + // TODO: try catch + let channel = try! GRPCChannelPool.with( + target: .unixDomainSocket(socketPath.path(percentEncoded: false)), + transportSecurity: .plaintext, + eventLoopGroup: group + ) + let apiClient = DotLocalAsyncClient(channel: channel) + self.apiClient = apiClient + + setState(.started) + } + + private func onStop() { + task = nil + apiClient = nil + setState(.stopped) + } + + func stop() { + guard let task = task else { + return + } + task.terminate() + } + + func wait() { + guard let task = task else { + return + } + task.waitUntilExit() + } + + private func setState(_ newState: DaemonStateInternal) { + internalState = newState + NSLog("new state: \(newState)") + _internalStates.send(newState) + } + + private static func mapState(_ internalState: DaemonStateInternal) async -> DaemonState { + switch internalState { + case .stopped: + return .stopped + case .starting: + return .starting + case .started: + guard let apiClient = DaemonManager.shared.apiClient else { + return .started(mappings: []) + } + let res = try? await apiClient.listMappings(.with({_ in})) + return .started(mappings: (res?.mappings)?.sorted() ?? []) + } + } + + func daemonState(provider: SequentialResultProvider) async { + var subscriptions = Set() + states.sink(receiveCompletion: { _ in + if !provider.isFinished { + provider.respond(withResult: .finished) + } + }, receiveValue: { + if !provider.isFinished { + provider.respond(withResult: .success($0)) + } else { + subscriptions.removeAll() + } + }) + .store(in: &subscriptions) + } +} + +enum DaemonStateInternal { + case stopped + case starting + case started +} diff --git a/DotLocalHelperTool/HelperToolConfig.xcconfig b/DotLocalHelperTool/HelperToolConfig.xcconfig new file mode 100644 index 0000000..fd4fabd --- /dev/null +++ b/DotLocalHelperTool/HelperToolConfig.xcconfig @@ -0,0 +1,32 @@ +// +// HelperToolConfig.xcconfig +// DotLocal +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +#include "Config/Config.xcconfig" + +// The directory containing the source code and property lists for the helper tool. +TARGET_DIRECTORY = DotLocalHelperTool + +// Bundle identifier used both in the info property list and so the build script knows which target it is running for. +// If you want to change the bundle identifier, change the value for HELPER_TOOL_BUNDLE_IDENTIFIER in Config.xcconfig. +PRODUCT_BUNDLE_IDENTIFIER = $(HELPER_TOOL_BUNDLE_IDENTIFIER) + +// Name of the executable created by the build process. To satisfy SMJobBless this must match the bundle identifier. +TARGET_NAME = $(HELPER_TOOL_BUNDLE_IDENTIFIER) + +// Property list locations +INFOPLIST_FILE = $(TARGET_DIRECTORY)/Info.plist +LAUNCHDPLIST_FILE = $(TARGET_DIRECTORY)/launchd.plist + +// Inlines the property list files into the helper tool's binary. +// Note that CREATE_INFOPLIST_SECTION_IN_BINARY = YES can't be used to inline the info property list because this step +// occurs immediately *before* any scripts are run, preventing the property list from being modified. +OTHER_LDFLAGS = -sectcreate __TEXT __info_plist $(INFOPLIST_FILE) -sectcreate __TEXT __launchd_plist $(LAUNCHDPLIST_FILE) + +SWIFT_ACTIVE_COMPILATION_CONDITIONS = HELPER_TOOL diff --git a/DotLocalHelperTool/Info.plist b/DotLocalHelperTool/Info.plist new file mode 100644 index 0000000..4edc715 --- /dev/null +++ b/DotLocalHelperTool/Info.plist @@ -0,0 +1,14 @@ + + + + + CFBundleIdentifier + dev.suphon.DotLocal.helper + CFBundleVersion + 0.0.0 + SMAuthorizedClients + + anchor apple generic and identifier "dev.suphon.DotLocal" and info[CFBundleVersion] >= "0.0.1" and certificate leaf[subject.OU] = "2HCNNF2TB5" + + + diff --git a/DotLocalHelperTool/ManageClient.swift b/DotLocalHelperTool/ManageClient.swift new file mode 100644 index 0000000..4f2b819 --- /dev/null +++ b/DotLocalHelperTool/ManageClient.swift @@ -0,0 +1,34 @@ +// +// ManageCLI.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation + +private let installLocation = URL.init(filePath: "/usr/local/bin/dotlocal") + +enum ManageClient { + static func install(bundleURL: URL) throws { + let clientURL = bundleURL.appending(path: "Contents/Resources/bin/dotlocal") + NSLog("installing client") + + guard try CodeInfo.doesPublicKeyMatch(forExecutable: clientURL) else { + NSLog("start daemon failed: security requirements not met") + return + } + + NSLog("symlink \(clientURL) to \(installLocation)") + try FileManager.default.createDirectory(at: installLocation.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink(at: installLocation, withDestinationURL: clientURL) + NSLog("installed client") + } + + static func uninstall() throws { + NSLog("uninstalling client") + NSLog("remove \(installLocation)") + try FileManager.default.removeItem(at: installLocation) + NSLog("uninstalled client") + } +} diff --git a/DotLocalHelperTool/Uninstaller.swift b/DotLocalHelperTool/Uninstaller.swift new file mode 100644 index 0000000..fd57b10 --- /dev/null +++ b/DotLocalHelperTool/Uninstaller.swift @@ -0,0 +1,124 @@ +// +// Uninstaller.swift +// SwiftAuthorizationSample +// +// Created by Josh Kaplan on 2021-10-24 +// + +import Foundation + +/// A self uninstaller which performs the logical equivalent of the non-existent `SMJobUnbless`. +/// +/// Because Apple does not provide an API to perform an "unbless" operation, the technique used here relies on a few key behaviors: +/// - To deregister the helper tool with launchd, the `launchctl` command line utility which ships with macOS is used +/// - The `unload` command used is publicly documented +/// - An assumption that this helper tool when installed is located at `/Library/PrivilegedHelperTools/` +/// - While this location is not documented in `SMJobBless`, it is used in Apple's EvenBetterAuthorizationSample `Uninstall.sh` script +/// - This is used to determine if this helper tool is in fact running from the blessed location +/// - To remove the `launchd` property list, its location is assumed to be `/Library/LaunchDaemons/.plist` +/// - While this location is not documented in `SMJobBless`, it is used in Apple's EvenBetterAuthorizationSample `Uninstall.sh` script +enum Uninstaller { + /// Errors that prevent the uninstall from succeeding. + enum UninstallError: Error { + /// Uninstall will not be performed because this code is not running from the blessed location. + case notRunningFromBlessedLocation(location: URL) + /// Attempting to unload using `launchctl` failed. + case launchctlFailure(statusCode: Int32) + /// The argument provided must be a process identifier, but was not. + case notProcessId(invalidArgument: String) + } + + /// Command line argument that identifies to `main` to call the `uninstallFromCommandLine(...)` function. + static let commandLineArgument = "uninstall" + + /// Indirectly uninstalls this helper tool. Calling this function will terminate this process unless an error is throw. + /// + /// Uninstalls this helper tool by relaunching itself not via XPC such that the installation can occur succesfully. + /// + /// - Throws: If unable to determine the on disk location of this running code. + static func uninstallFromXPC() throws { + NSLog("uninstall requested, PID \(getpid())") + let process = Process() + process.launchPath = try CodeInfo.currentCodeLocation().path + process.qualityOfService = QualityOfService.utility + process.arguments = [commandLineArgument, String(getpid())] + process.launch() + NSLog("about to exit...") + exit(0) + } + + /// Directly uninstalls this executable. Calling this function will terminate this process unless an error is thrown. + /// + /// Depending on the the arguments provided to this function, it may wait on another process to exit before performing the uninstall. + /// + /// - Parameter arguments: The command line arguments; the first argument should always be `uninstall`. + /// - Throws: If unable to perform the uninstall, including because the provided arguments are invalid. + static func uninstallFromCommandLine(withArguments arguments: [String]) throws -> Never { + if arguments.count == 1 { + try uninstallImmediately() + } else { + guard let pid: pid_t = Int32(arguments[1]) else { + throw UninstallError.notProcessId(invalidArgument: arguments[1]) + } + try uninstallAfterProcessExits(pid: pid) + } + } + + /// Uninstalls this helper tool after waiting for a process (in practice this helper tool launched via XPC) to terminate. + /// + /// - Parameter pid: Identifier for the process which must terminate before performing uninstall. In practice this is identifier is for is a previous run of this helper tool. + private static func uninstallAfterProcessExits(pid: pid_t) throws -> Never { + // When passing 0 as the second argument, no signal is sent, but existence and permission checks are still + // performed. This checks for the existence of a process ID. If 0 is returned the process still exists, so loop + // until 0 is no longer returned. + while kill(pid, 0) == 0 { // in practice this condition almost always evaluates to false on its first check + usleep(50 * 1000) // sleep for 50ms + NSLog("PID \(getpid()) waited 50ms for PID \(pid)") + } + NSLog("PID \(getpid()) done waiting for PID \(pid)") + + try uninstallImmediately() + } + + /// Uninstalls this helper tool. + /// + /// This function will not work if called when this helper tool was started by an XPC call because `launchctl` will be unable to unload. + /// + /// If the uninstall fails when deleting either the `launchd` property list for this executable or the on disk representation of this helper tool then the uninstall + /// will be an incomplete state; however, it will no longer be started by `launchd` (and in turn not accessible via XPC) and so will be mostly uninstalled even + /// though some on disk portions will remain. + /// + /// - Throws: Due to one of: unable to determine the on disk location of this running code, that location is not the blessed location, `launchctl` can't + /// unload this helper tool, the `launchd` property list for this helper tool can't be deleted, or the on disk representation of this helper tool can't be deleted. + private static func uninstallImmediately() throws -> Never { + let sharedConstants = try SharedConstants() + let currentLocation = try CodeInfo.currentCodeLocation() + guard currentLocation == sharedConstants.blessedLocation else { + throw UninstallError.notRunningFromBlessedLocation(location: currentLocation) + } + + // Equivalent to: launchctl unload /Library/LaunchDaemons/.plist + let process = Process() + process.launchPath = "/bin/launchctl" + process.qualityOfService = QualityOfService.utility + process.arguments = ["unload", sharedConstants.blessedPropertyListLocation.path] + process.launch() + NSLog("about to wait for launchctl...") + process.waitUntilExit() + let terminationStatus = process.terminationStatus + guard terminationStatus == 0 else { + throw UninstallError.launchctlFailure(statusCode: terminationStatus) + } + NSLog("unloaded from launchctl") + + // Equivalent to: rm /Library/LaunchDaemons/.plist + try FileManager.default.removeItem(at: sharedConstants.blessedPropertyListLocation) + NSLog("property list deleted") + + // Equivalent to: rm /Library/PrivilegedHelperTools/ + try FileManager.default.removeItem(at: sharedConstants.blessedLocation) + NSLog("helper tool deleted") + NSLog("uninstall completed, exiting...") + exit(0) + } +} diff --git a/DotLocalHelperTool/Updater.swift b/DotLocalHelperTool/Updater.swift new file mode 100644 index 0000000..f3a8270 --- /dev/null +++ b/DotLocalHelperTool/Updater.swift @@ -0,0 +1,67 @@ +// +// Updater.swift +// SwiftAuthorizationSample +// +// Created by Josh Kaplan on 2021-10-24 +// + +import Foundation +import EmbeddedPropertyList + +/// An in-place updater for the helper tool. +/// +/// To keep things simple, this updater only works if `launchd` property lists do not change between versions. +enum Updater { + /// Replaces itself with the helper tool located at the provided `URL` so long as security, launchd, and version requirements are met. + /// + /// - Parameter helperTool: Path to the helper tool. + /// - Throws: If the helper tool file can't be read, public keys can't be determined, or `launchd` property lists can't be compared. + static func updateHelperTool(atPath helperTool: URL) throws { + guard try CodeInfo.doesPublicKeyMatch(forExecutable: helperTool) else { + NSLog("update failed: security requirements not met") + return + } + + guard try launchdPropertyListsMatch(forHelperTool: helperTool) else { + NSLog("update failed: launchd property list has changed") + return + } + + let (isNewer, currentVersion, otherVersion) = try isHelperToolNewerVersion(atPath: helperTool) + guard isNewer else { + NSLog("update failed: not a newer version. current: \(currentVersion), other: \(otherVersion).") + return + } + + try Data(contentsOf: helperTool).write(to: CodeInfo.currentCodeLocation(), options: .atomicWrite) + NSLog("update succeeded: current version \(currentVersion) exiting...") + exit(0) + } + + /// Determines if the helper tool located at the provided `URL` is actually an update. + /// + /// - Parameter helperTool: Path to the helper tool. + /// - Throws: If unable to read the info property lists of this helper tool or the one located at `helperTool`. + /// - Returns: If the helper tool at the location specified by `helperTool` is newer than the one running this code and the versions of both. + private static func isHelperToolNewerVersion( + atPath helperTool: URL + ) throws -> (isNewer: Bool, current: BundleVersion, other: BundleVersion) { + let current = try HelperToolInfoPropertyList.main.version + let other = try HelperToolInfoPropertyList(from: helperTool).version + + return (other >= current, current, other) + } + + /// Determines if the `launchd` property list used by this helper tool and the executable located at the provided `URL` are byte-for-byte identical. + /// + /// This matters because only the helper tool itself is being updated, the property list generated for `launchd` will not be updated as part of this update + /// process. + /// + /// - Parameter helperTool: Path to the helper tool. + /// - Throws: If unable to read the `launchd` property lists of this helper tool or the one located at `helperTool`. + /// - Returns: If the two `launchd` property lists match. + private static func launchdPropertyListsMatch(forHelperTool helperTool: URL) throws -> Bool { + try EmbeddedPropertyListReader.launchd.readInternal() == + EmbeddedPropertyListReader.launchd.readExternal(from: helperTool) + } +} diff --git a/DotLocalHelperTool/launchd.plist b/DotLocalHelperTool/launchd.plist new file mode 100644 index 0000000..276292b --- /dev/null +++ b/DotLocalHelperTool/launchd.plist @@ -0,0 +1,19 @@ + + + + + AssociatedBundleIdentifiers + + dev.suphon.DotLocal + + Label + dev.suphon.DotLocal.helper + MachServices + + dev.suphon.DotLocal.helper + + + StandardOutPath + /tmp/dotlocal.out + + diff --git a/DotLocalHelperTool/main.swift b/DotLocalHelperTool/main.swift new file mode 100644 index 0000000..a1a3469 --- /dev/null +++ b/DotLocalHelperTool/main.swift @@ -0,0 +1,74 @@ +// +// main.swift +// DotLocalHelperTool +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +import Foundation +import SecureXPC +import Dispatch + +NSLog("starting helper tool. PID \(getpid()). PPID \(getppid()).") +NSLog("version: \(try HelperToolInfoPropertyList.main.version.rawValue)") +NSLog("code location: \(String(describing: try? CodeInfo.currentCodeLocation()))") + +// Command line arguments were provided, so process them +if CommandLine.arguments.count > 1 { + // Remove the first argument, which represents the name (typically the full path) of this helper tool + var arguments = CommandLine.arguments + _ = arguments.removeFirst() + NSLog("run with arguments: \(arguments)") + + if let firstArgument = arguments.first { + if firstArgument == Uninstaller.commandLineArgument { + try Uninstaller.uninstallFromCommandLine(withArguments: arguments) + } else { + NSLog("argument not recognized: \(firstArgument)") + } + } +} else if getppid() == 1 { // Otherwise if started by launchd, start up server + let server = try XPCServer.forMachService() + + server.registerRoute(SharedConstants.startDaemonRoute, handler: DaemonManager.shared.start) + server.registerRoute(SharedConstants.stopDaemonRoute, handler: DaemonManager.shared.stop) + server.registerRoute(SharedConstants.daemonStateRoute, handler: DaemonManager.shared.daemonState) + + server.registerRoute(SharedConstants.installClientRoute, handler: ManageClient.install) + server.registerRoute(SharedConstants.uninstallClientRoute, handler: ManageClient.uninstall) + + server.registerRoute(SharedConstants.exitRoute, handler: gracefulExit) + + server.registerRoute(SharedConstants.uninstallRoute, handler: Uninstaller.uninstallFromXPC) + server.registerRoute(SharedConstants.updateRoute, handler: Updater.updateHelperTool(atPath:)) + + server.setErrorHandler { error in + if case .connectionInvalid = error { + // Ignore invalidated connections as this happens whenever the client disconnects which is not a problem + } else { + NSLog("error: \(error)") + } + } + + signal(SIGINT, SIG_IGN) + signal(SIGTERM, SIG_IGN) + + let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main) + sigintSrc.setEventHandler(handler: gracefulExit) + sigintSrc.resume() + let sigtermSrc = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main) + sigtermSrc.setEventHandler(handler: gracefulExit) + sigtermSrc.resume() + + server.startAndBlock() +} else { // Otherwise started via command line without arguments, print out help info + print("Usage: \(try CodeInfo.currentCodeLocation().lastPathComponent) ") + print("\nCommands:") + print("\t\(Uninstaller.commandLineArgument)\tUnloads and deletes from disk this helper tool and configuration.") +} + +func gracefulExit() { + NSLog("exiting") + DaemonManager.shared.stop() + exit(0) +} diff --git a/PropertyListModifier/main.swift b/PropertyListModifier/main.swift new file mode 100644 index 0000000..e662414 --- /dev/null +++ b/PropertyListModifier/main.swift @@ -0,0 +1,547 @@ +// +// PropertyListModifier.swift +// SwiftAuthorizationSample +// +// Created by Josh Kaplan on 2021-10-23 +// + +// This script generates all of the property list requirements needed by SMJobBless and XPC Mach Services in conjunction +// with user defined variables specified in the xcconfig files. This scripts adds/modifies/removes entries to: +// - App's info property list +// - Helper tool's info property list +// - Helper tool's launchd property list +// +// For this sample, this script is run both at the beginning and end of the build process for both targets. When run at +// the end it deletes all of the property list requirement changes it applied. For your own project you may not find it +// useful to do this cleanup task at the end of the build process. +// +// Additionally this script can auto-increment the helper tools's version number whenever the source code changes +// - SMJobBless will only successfully install a new helper tool over an existing one if its version is greater +// - In order to track changes, a BuildHash entry will be added to the helper tool's Info property list +// - This sample is configured by default to perform this auto-increment +// +// All of these options are configured by passing in command line arguments to this script. See ScriptTask for details. + +import Foundation +import CryptoKit + +/// Errors raised throughout this script. +enum ScriptError: Error { + case general(String) + case wrapped(String, Error) +} + +// MARK: helper functions to read environment variables + +/// Attempts to read an environment variable, throws an error if it is not present. +/// +/// - Parameters: +/// - name: Name of the environment variable. +/// - description: A description of what was trying to be read; used in the error message if one is thrown. +/// - isUserDefined: Whether the environment variable is user defined; used to modify the error message if one is thrown. +func readEnvironmentVariable(name: String, description: String, isUserDefined: Bool) throws -> String { + if let value = ProcessInfo.processInfo.environment[name] { + return value + } else { + var message = "Unable to determine \(description), missing \(name) environment variable." + if isUserDefined { + message += " This is a user-defined variable. Please check that the xcconfig files are present and " + + "configured in the project settings." + } + throw ScriptError.general(message) + } +} + +/// Attempts to read an environment variable as a URL. +func readEnvironmentVariableAsURL(name: String, description: String, isUserDefined: Bool) throws -> URL { + let value = try readEnvironmentVariable(name: name, description: description, isUserDefined: isUserDefined) + + return URL(fileURLWithPath: value) +} + +// MARK: property list keys + +// Helper tool - info +/// Key for entry in helper tool's info property list. +let SMAuthorizedClientsKey = "SMAuthorizedClients" +/// Key for bundle identifier. +let CFBundleIdentifierKey = kCFBundleIdentifierKey as String +/// Key for bundle version. +let CFBundleVersionKey = kCFBundleVersionKey as String +/// Custom key for an entry in the helper tool's info plist that contains a hash of source files. Used to detect when the build changes. +let BuildHashKey = "BuildHash" + +// Helper tool - launchd +/// Key for entry in helper tool's launchd property list. +let LabelKey = "Label" +/// Key for XPC mach service used by the helper tool. +let MachServicesKey = "MachServices" + +// App - info +/// Key for entry in app's info property list. +let SMPrivilegedExecutablesKey = "SMPrivilegedExecutables" + +// MARK: code signing requirements + +/// A requirement that the organizational unit for the leaf certificate match the development team identifier. +/// +/// From Apple's documentation: "In Apple issued developer certificates, this field contains the developer’s Team Identifier." +/// +/// The leaf certificate is the one which corresponds to your developer certificate. The certificates above it in the chain are Apple's. +/// Depending on whether this build is signed for debug or release the leaf certificate *will* differ, but the organizational unit, represented by `subject.OU` in +/// the function, will remain the same. +func organizationalUnitRequirement() throws -> String { + // In order for this requirement to actually work, the signed app or helper tool needs to have a certificate chain + // which will contain the organizational unit (subject.OU). While it'd be great if we could just examine the signed + // app/helper tool after that's been done, that's of course not possible as we need to generate this requirement + // *during* the process for each. + // + // Note: In practice this certificate chain won't exist when self signing using "Sign to Run Locally". + // + // There's no to precise way to determine if the subject.OU will be present, but in practice we can check for the + // subject.CN (CN stands for common name) by seeing if there is a meaningful value for the CODE_SIGN_IDENTITY + // build variable. This could still fail because we're checking for *this* target's common name, but creating an + // identity for the *other* target - so if Xcode isn't configured the same for both targets an issue is likely to + // arise. + // + // Note: The reason to use the organizational unit for the code requirement instead of the common name is because + // the organizational unit will be consistent between the Apple Development and Developer ID builds, while the + // common name will not be — simplifying the development workflow. + let commonName = ProcessInfo.processInfo.environment["CODE_SIGN_IDENTITY"] + if commonName == nil || commonName == "-" { + throw ScriptError.general("Signing Certificate must be Development. Sign to Run Locally is not supported.") + } + + let developmentTeamId = try readEnvironmentVariable(name: "DEVELOPMENT_TEAM", + description: "development team for code signing", + isUserDefined: false) + guard developmentTeamId.range(of: #"^[A-Z0-9]{10}$"#, options: .regularExpression) != nil else { + if developmentTeamId == "-" { + throw ScriptError.general("Development Team for code signing is not set") + } else { + throw ScriptError.general("Development Team for code signing is invalid: \(developmentTeamId)") + } + } + let certificateString = "certificate leaf[subject.OU] = \"\(developmentTeamId)\"" + + return certificateString +} + +/// Requirement that Apple is part of the certificate chain, mean it was signed by an Apple issued certificate +let appleGenericRequirement = "anchor apple generic" + +/// Creates a `SMAuthorizedClients` entry representing the app which must go inside the helper tool's info property list. +func SMAuthorizedClientsEntry() throws -> (key: String, value: [String]) { + let appIdentifierRequirement = "identifier \"\(try TargetType.app.bundleIdentifier())\"" + // Create requirement that the app must be its current version or later. This mitigates downgrade attacks where an + // older version of the app had a security vulnerability fixed in later versions. The attacker could then + // intentionally install and run an older version of the app and exploit its vulnerability in order to talk to + // the helper tool. + let appVersion = try readEnvironmentVariable(name: "APP_VERSION", + description: "app version", + isUserDefined: true) + let appVersionRequirement = "info[\(CFBundleVersionKey)] >= \"\(appVersion)\"" + let requirements = [appleGenericRequirement, + appIdentifierRequirement, + appVersionRequirement, + try organizationalUnitRequirement()] + let value = [requirements.joined(separator: " and ")] + + return (SMAuthorizedClientsKey, value) +} + +/// Creates a `SMPrivilegedExecutables` entry representing the helper tool which must go inside the app's info property list. +func SMPrivilegedExecutablesEntry() throws -> (key: String, value: [String : String]) { + let helperToolIdentifierRequirement = "identifier \"\(try TargetType.helperTool.bundleIdentifier())\"" + let requirements = [appleGenericRequirement, helperToolIdentifierRequirement, try organizationalUnitRequirement()] + let value = [try TargetType.helperTool.bundleIdentifier() : requirements.joined(separator: " and ")] + + return (SMPrivilegedExecutablesKey, value) +} + +/// Creates a `Label` entry which must go inside the helper tool's launchd property list. +func LabelEntry() throws -> (key: String, value: String) { + return (key: LabelKey, value: try TargetType.helperTool.bundleIdentifier()) +} + +// MARK: property list manipulation + +/// Reads the property list at the provided path. +/// +/// - Parameters: +/// - atPath: Where the property list is located. +/// - Returns: Tuple containing entries and the format of the on disk property list. +func readPropertyList(atPath path: URL) throws -> (entries: NSMutableDictionary, + format: PropertyListSerialization.PropertyListFormat) { + let onDiskPlistData: Data + do { + onDiskPlistData = try Data(contentsOf: path) + } catch { + throw ScriptError.wrapped("Unable to read property list at: \(path)", error) + } + + do { + var format = PropertyListSerialization.PropertyListFormat.xml + let plist = try PropertyListSerialization.propertyList(from: onDiskPlistData, + options: .mutableContainersAndLeaves, + format: &format) + if let entries = plist as? NSMutableDictionary { + return (entries: entries, format: format) + } + else { + throw ScriptError.general("Unable to cast parsed property list") + } + } + catch { + throw ScriptError.wrapped("Unable to parse property list", error) + } +} + +/// Writes (or overwrites) a property list at the provided path. +/// +/// - Parameters: +/// - atPath: Where the property list should be written. +/// - entries: All entries to be written to the property list, this does not append - it overwrites anything existing. +/// - format:The format to use when writing entries to `atPath`. +func writePropertyList(atPath path: URL, + entries: NSDictionary, + format: PropertyListSerialization.PropertyListFormat) throws { + let plistData: Data + do { + plistData = try PropertyListSerialization.data(fromPropertyList: entries, + format: format, + options: 0) + } catch { + throw ScriptError.wrapped("Unable to serialize property list in order to write to path: \(path)", error) + } + + do { + try plistData.write(to: path) + } + catch { + throw ScriptError.wrapped("Unable to write property list to path: \(path)", error) + } +} + +/// Updates the property list with the provided entries. +/// +/// If an existing entry exists for the given key it will be overwritten. If the property file does not exist, it will be created. +func updatePropertyListWithEntries(_ newEntries: [String : AnyHashable], atPath path: URL) throws { + let (entries, format) : (NSMutableDictionary, PropertyListSerialization.PropertyListFormat) + if FileManager.default.fileExists(atPath: path.path) { + (entries, format) = try readPropertyList(atPath: path) + } else { + (entries, format) = ([:], PropertyListSerialization.PropertyListFormat.xml) + } + for (key, value) in newEntries { + entries.setValue(value, forKey: key) + } + try writePropertyList(atPath: path, entries: entries, format: format) +} + +/// Updates the property list by removing the provided keys (if present) or deletes the file if there are now no entries. +func removePropertyListEntries(forKeys keys: [String], atPath path: URL) throws { + let (entries, format) = try readPropertyList(atPath: path) + for key in keys { + entries.removeObject(forKey: key) + } + + if entries.count > 0 { + try writePropertyList(atPath: path, entries: entries, format: format) + } else { + try FileManager.default.removeItem(at: path) + } +} + +/// The path of the info property list for this target. +func infoPropertyListPath() throws -> URL { + return try readEnvironmentVariableAsURL(name: "INFOPLIST_FILE", + description: "info property list path", + isUserDefined: true) +} + +/// The path of the launchd property list for the helper tool. +func launchdPropertyListPath() throws -> URL { + try readEnvironmentVariableAsURL(name: "LAUNCHDPLIST_FILE", + description: "launchd property list path", + isUserDefined: true) +} + +// MARK: automatic bundle version updating + +/// Hashes Swift source files in the helper tool's directory as well as the shared directory. +/// +/// - Returns: hash value, hex encoded +func hashSources() throws -> String { + // Directories to hash source files in + let sourcePaths: [URL] = [ + try infoPropertyListPath().deletingLastPathComponent(), + try readEnvironmentVariableAsURL(name: "SHARED_DIRECTORY", + description: "shared source directory path", + isUserDefined: true) + ] + + // Enumerate over and hash Swift source files + var sha256 = SHA256() + for sourcePath in sourcePaths { + if let enumerator = FileManager.default.enumerator(at: sourcePath, includingPropertiesForKeys: []) { + for case let fileURL as URL in enumerator { + if fileURL.pathExtension == "swift" { + do { + sha256.update(data: try Data(contentsOf: fileURL)) + } catch { + throw ScriptError.wrapped("Unable to hash \(fileURL)", error) + } + } + } + } else { + throw ScriptError.general("Could not create enumerator for: \(sourcePath)") + } + } + let digestHex = sha256.finalize().compactMap{ String(format: "%02x", $0) }.joined() + + return digestHex +} + +/// Represents the value corresponding to the key `CFBundleVersionKey` in the info property list. +enum BundleVersion { + case major(UInt) + case majorMinor(UInt, UInt) + case majorMinorPatch(UInt, UInt, UInt) + + init?(version: String) { + let versionParts = version.split(separator: ".") + if versionParts.count == 1, + let major = UInt(versionParts[0]) { + self = .major(major) + } + else if versionParts.count == 2, + let major = UInt(versionParts[0]), + let minor = UInt(versionParts[1]) { + self = .majorMinor(major, minor) + } + else if versionParts.count == 3, + let major = UInt(versionParts[0]), + let minor = UInt(versionParts[1]), + let patch = UInt(versionParts[2]) { + self = .majorMinorPatch(major, minor, patch) + } + else { + return nil + } + } + + var version: String { + switch self { + case .major(let major): + return "\(major)" + case .majorMinor(let major, let minor): + return "\(major).\(minor)" + case .majorMinorPatch(let major, let minor, let patch): + return "\(major).\(minor).\(patch)" + } + } + + func increment() -> BundleVersion { + switch self { + case .major(let major): + return .major(major + 1) + case .majorMinor(let major, let minor): + return .majorMinor(major, minor + 1) + case .majorMinorPatch(let major, let minor, let patch): + return .majorMinorPatch(major, minor, patch + 1) + } + } +} + +/// Reads the `CFBundleVersion` value from the passed in dictionary. +func readBundleVersion(propertyList: NSMutableDictionary) throws -> BundleVersion { + if let value = propertyList[CFBundleVersionKey] as? String { + if let version = BundleVersion(version: value) { + return version + } else { + throw ScriptError.general("Invalid value for \(CFBundleVersionKey) in property list") + } + } else { + throw ScriptError.general("Could not find version, \(CFBundleVersionKey) missing in property list") + } +} + +/// Reads the `BuildHash` value from the passed in dictionary. +func readBuildHash(propertyList: NSMutableDictionary) throws -> String? { + return propertyList[BuildHashKey] as? String +} + +/// Reads the info property list, determines if the build has changed based on stored hash values, and increments the build version if it has. +func incrementBundleVersionIfNeeded(infoPropertyListPath: URL) throws { + let propertyList = try readPropertyList(atPath: infoPropertyListPath) + let previousBuildHash = try readBuildHash(propertyList: propertyList.entries) + let currentBuildHash = try hashSources() + if currentBuildHash != previousBuildHash { + let version = try readBundleVersion(propertyList: propertyList.entries) + let newVersion = version.increment() + + propertyList.entries[BuildHashKey] = currentBuildHash + propertyList.entries[CFBundleVersionKey] = newVersion.version + + try writePropertyList(atPath: infoPropertyListPath, + entries: propertyList.entries, + format: propertyList.format) + } +} + +// MARK: Xcode target + +/// The two build targets used as part of this sample. +enum TargetType: String { + case app = "APP_BUNDLE_IDENTIFIER" + case helperTool = "HELPER_TOOL_BUNDLE_IDENTIFIER" + + func bundleIdentifier() throws -> String { + return try readEnvironmentVariable(name: self.rawValue, + description: "bundle identifier for \(self)", + isUserDefined: true) + } +} + +/// Determines whether this script is running for the app or the helper tool. +func determineTargetType() throws -> TargetType { + let bundleId = try readEnvironmentVariable(name: "PRODUCT_BUNDLE_IDENTIFIER", + description: "bundle id", + isUserDefined: false) + + let appBundleIdentifier = try TargetType.app.bundleIdentifier() + let helperToolBundleIdentifier = try TargetType.helperTool.bundleIdentifier() + if bundleId == appBundleIdentifier { + return TargetType.app + } else if bundleId == helperToolBundleIdentifier { + return TargetType.helperTool + } else { + throw ScriptError.general("Unexpected bundle id \(bundleId) encountered. This means you need to update the " + + "user defined variables APP_BUNDLE_IDENTIFIER and/or " + + "HELPER_TOOL_BUNDLE_IDENTIFIER in Config.xcconfig.") + } +} + +// MARK: tasks + +/// The tasks this script can perform. They're provided as command line arguments to this script. +typealias ScriptTask = () throws -> Void +let scriptTasks: [String : ScriptTask] = [ + /// Update the property lists as needed to satisfy the requirements of SMJobBless + "satisfy-job-bless-requirements" : satisfyJobBlessRequirements, + /// Clean up changes made to property lists to satisfy the requirements of SMJobBless + "cleanup-job-bless-requirements" : cleanupJobBlessRequirements, + /// Specifies MachServices entry in the helper tool's launchd property list to enable XPC + "specify-mach-services" : specifyMachServices, + /// Cleans up changes made to Mach Services in the helper tool's launchd property list + "cleanup-mach-services" : cleanupMachServices, + /// Auto increment the bundle version number; only intended for the helper tool. + "auto-increment-version" : autoIncrementVersion +] + +/// Determines what tasks this script should undertake in based on passed in arguments. +func determineScriptTasks() throws -> [ScriptTask] { + if CommandLine.arguments.count > 1 { + var matchingTasks = [ScriptTask]() + for index in 1.. URL { + var path: CFURL? + let status = SecCodeCopyPath(try copyCurrentStaticCode(), SecCSFlags(), &path) + guard status == errSecSuccess, let path = path as URL? else { + throw CodeInfoError.codeLocationNotRetrievable(status) + } + + return path + } + + /// Determines if the public keys of this helper tool and the executable corresponding to the passed in `URL` match. + /// + /// - Parameter executable: On disk location of an executable. + /// - Throws: If unable to compare the public keys for the on disk representations of both this helper tool and the executable for the provided URL. + /// - Returns: If the public keys of their leaf certificates (which is the Developer ID certificate) match. + static func doesPublicKeyMatch(forExecutable executable: URL) throws -> Bool { + // Only perform this comparison if the executable's static code has a valid signature + let executableStaticCode = try createStaticCode(forExecutable: executable) + let checkFlags = SecCSFlags(rawValue: kSecCSStrictValidate | kSecCSCheckAllArchitectures) + guard SecStaticCodeCheckValidity(executableStaticCode, checkFlags, nil) == errSecSuccess else { + return false + } + + let currentKeyData = try copyLeafCertificateKeyData(staticCode: try copyCurrentStaticCode()) + let executableKeyData = try copyLeafCertificateKeyData(staticCode: executableStaticCode) + + return currentKeyData == executableKeyData + } + + /// Convenience wrapper around `SecStaticCodeCreateWithPath`. + /// + /// - Parameter executable: On disk location of an executable. + /// - Throws: If unable to create the static code. + /// - Returns: Static code instance corresponding to the provided `URL`. + static func createStaticCode(forExecutable executable: URL) throws -> SecStaticCode { + var staticCode: SecStaticCode? + let status = SecStaticCodeCreateWithPath(executable as CFURL, SecCSFlags(), &staticCode) + guard status == errSecSuccess, let staticCode = staticCode else { + throw CodeInfoError.externalStaticCodeNotRetrievable(status) + } + + return staticCode + } + + /// Convenience wrapper around `SecCodeCopySelf` and `SecCodeCopyStaticCode`. + /// + /// - Throws: If unable to create a copy of the on disk representation of this code. + /// - Returns: Static code instance corresponding to the executable running this code. + static func copyCurrentStaticCode() throws -> SecStaticCode { + var currentCode: SecCode? + let copySelfStatus = SecCodeCopySelf(SecCSFlags(), ¤tCode) + guard copySelfStatus == errSecSuccess, let currentCode = currentCode else { + throw CodeInfoError.helperToolStaticCodeNotRetrievable(copySelfStatus) + } + + var currentStaticCode: SecStaticCode? + let staticCodeStatus = SecCodeCopyStaticCode(currentCode, SecCSFlags(), ¤tStaticCode) + guard staticCodeStatus == errSecSuccess, let currentStaticCode = currentStaticCode else { + throw CodeInfoError.helperToolStaticCodeNotRetrievable(staticCodeStatus) + } + + return currentStaticCode + } + + /// Returns the leaf certificate in the code's certificate chain. + /// + /// For a Developer ID signed app, this practice this corresponds to the Developer ID certificate. + /// + /// - Parameter staticCode: On disk representation. + /// - Throws: If unable to determine the certificate. + /// - Returns: The leaf certificate. + static func copyLeafCertificate(staticCode: SecStaticCode) throws -> SecCertificate { + var info: CFDictionary? + let flags = SecCSFlags(rawValue: kSecCSSigningInformation) + guard SecCodeCopySigningInformation(staticCode, flags, &info) == errSecSuccess, + let info = info as NSDictionary?, + let certificates = info[kSecCodeInfoCertificates as String] as? [SecCertificate], + let leafCertificate = certificates.first else { + throw CodeInfoError.leafCertificateNotRetrievable + } + + return leafCertificate + } + + /// Returns the signing key in data form for the leaf certificate in the certificate chain. + /// + /// - Parameter staticCode: On disk representation. + /// - Throws: If unable to copy the data. + /// - Returns: Signing key in data form for the leaf certificate in the certificate chain. + private static func copyLeafCertificateKeyData(staticCode: SecStaticCode) throws -> Data { + guard let leafKey = SecCertificateCopyKey(try copyLeafCertificate(staticCode: staticCode)), + let leafKeyData = SecKeyCopyExternalRepresentation(leafKey, nil) as Data? else { + throw CodeInfoError.signingKeyDataNotRetrievable + } + + return leafKeyData + } +} diff --git a/Shared/DaemonState.swift b/Shared/DaemonState.swift new file mode 100644 index 0000000..5cae860 --- /dev/null +++ b/Shared/DaemonState.swift @@ -0,0 +1,15 @@ +// +// DaemonState.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation + +enum DaemonState: Codable { + case unknown + case stopped + case starting + case started(mappings: [Mapping]) +} diff --git a/Shared/HelperToolInfoPropertyList.swift b/Shared/HelperToolInfoPropertyList.swift new file mode 100644 index 0000000..d326e29 --- /dev/null +++ b/Shared/HelperToolInfoPropertyList.swift @@ -0,0 +1,42 @@ +// +// HelperToolInfoPropertyList.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +import Foundation +import EmbeddedPropertyList + +/// Read only representation of the helper tool's info property list. +struct HelperToolInfoPropertyList: Decodable { + /// Value for `SMAuthorizedClients`. +// let authorizedClients: [String] + /// Value for `CFBundleVersion`. + let version: BundleVersion + /// Value for `CFBundleIdentifier`. + let bundleIdentifier: String + + // Used by the decoder to map the names of the entries in the property list to the property names of this struct + private enum CodingKeys: String, CodingKey { +// case authorizedClients = "SMAuthorizedClients" + case version = "CFBundleVersion" + case bundleIdentifier = "CFBundleIdentifier" + } + + /// An immutable in memory representation of the property list by attempting to read it from the helper tool. + static var main: HelperToolInfoPropertyList { + get throws { + try PropertyListDecoder().decode(HelperToolInfoPropertyList.self, + from: try EmbeddedPropertyListReader.info.readInternal()) + } + } + + /// Creates an immutable in memory representation of the property list by attempting to read it from the helper tool. + /// + /// - Parameter url: Location of the helper tool on disk. + init(from url: URL) throws { + self = try PropertyListDecoder().decode(HelperToolInfoPropertyList.self, + from: try EmbeddedPropertyListReader.info.readExternal(from: url)) + } +} diff --git a/Shared/HelperToolLaunchdPropertyList.swift b/Shared/HelperToolLaunchdPropertyList.swift new file mode 100644 index 0000000..9695c3e --- /dev/null +++ b/Shared/HelperToolLaunchdPropertyList.swift @@ -0,0 +1,39 @@ +// +// HelperToolInfoPropertyList.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +import Foundation +import EmbeddedPropertyList + +/// Read only representation of the helper tool's embedded launchd property list. +struct HelperToolLaunchdPropertyList: Decodable { + /// Value for `MachServices`. + let machServices: [String : Bool] + /// Value for `Label`. + let label: String + + // Used by the decoder to map the names of the entries in the property list to the property names of this struct + private enum CodingKeys: String, CodingKey { + case machServices = "MachServices" + case label = "Label" + } + + /// An immutable in memory representation of the property list by attempting to read it from the helper tool. + static var main: HelperToolLaunchdPropertyList { + get throws { + try PropertyListDecoder().decode(HelperToolLaunchdPropertyList.self, + from: try EmbeddedPropertyListReader.launchd.readInternal()) + } + } + + /// Creates an immutable in memory representation of the property list by attempting to read it from the helper tool. + /// + /// - Parameter url: Location of the helper tool on disk. + init(from url: URL) throws { + self = try PropertyListDecoder().decode(HelperToolLaunchdPropertyList.self, + from: try EmbeddedPropertyListReader.launchd.readExternal(from: url)) + } +} diff --git a/Shared/Model/ProtoExtensions.swift b/Shared/Model/ProtoExtensions.swift new file mode 100644 index 0000000..4e30c3c --- /dev/null +++ b/Shared/Model/ProtoExtensions.swift @@ -0,0 +1,42 @@ +// +// ProtoExtensions.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 6/1/2567 BE. +// + +import Foundation + +extension Mapping: Identifiable {} + +extension Mapping: Decodable { + public init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + self = try Mapping(serializedData: try container.decode(Data.self)) + } catch { + print("error decoding: \(error)") + throw error + } + } +} + +extension Mapping: Encodable { + public func encode(to encoder: Encoder) throws { + do { + var container = encoder.singleValueContainer() + try container.encode(try serializedData()) + } catch { + NSLog("error encoding: \(error)") + throw error + } + } +} + +extension Mapping: Comparable { + public static func < (lhs: Mapping, rhs: Mapping) -> Bool { + let lhsTitle = "\(lhs.host)\(lhs.pathPrefix)" + let rhsTitle = "\(rhs.host)\(rhs.pathPrefix)" + return lhsTitle < rhsTitle + } +} diff --git a/DotLocal/Model/proto/dot-local.grpc.swift b/Shared/Model/proto/dot-local.grpc.swift similarity index 95% rename from DotLocal/Model/proto/dot-local.grpc.swift rename to Shared/Model/proto/dot-local.grpc.swift index 693f014..9d88a3a 100644 --- a/DotLocal/Model/proto/dot-local.grpc.swift +++ b/Shared/Model/proto/dot-local.grpc.swift @@ -19,7 +19,7 @@ public protocol DotLocalClientProtocol: GRPCClient { func createMapping( _ request: CreateMappingRequest, callOptions: CallOptions? - ) -> UnaryCall + ) -> UnaryCall func removeMapping( _ request: MappingKey, @@ -46,7 +46,7 @@ extension DotLocalClientProtocol { public func createMapping( _ request: CreateMappingRequest, callOptions: CallOptions? = nil - ) -> UnaryCall { + ) -> UnaryCall { return self.makeUnaryCall( path: DotLocalClientMetadata.Methods.createMapping.path, request: request, @@ -157,7 +157,7 @@ public protocol DotLocalAsyncClientProtocol: GRPCClient { func makeCreateMappingCall( _ request: CreateMappingRequest, callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall + ) -> GRPCAsyncUnaryCall func makeRemoveMappingCall( _ request: MappingKey, @@ -183,7 +183,7 @@ extension DotLocalAsyncClientProtocol { public func makeCreateMappingCall( _ request: CreateMappingRequest, callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { + ) -> GRPCAsyncUnaryCall { return self.makeAsyncUnaryCall( path: DotLocalClientMetadata.Methods.createMapping.path, request: request, @@ -222,7 +222,7 @@ extension DotLocalAsyncClientProtocol { public func createMapping( _ request: CreateMappingRequest, callOptions: CallOptions? = nil - ) async throws -> SwiftProtobuf.Google_Protobuf_Empty { + ) async throws -> Mapping { return try await self.performAsyncUnaryCall( path: DotLocalClientMetadata.Methods.createMapping.path, request: request, @@ -276,7 +276,7 @@ public struct DotLocalAsyncClient: DotLocalAsyncClientProtocol { public protocol DotLocalClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'createMapping'. - func makeCreateMappingInterceptors() -> [ClientInterceptor] + func makeCreateMappingInterceptors() -> [ClientInterceptor] /// - Returns: Interceptors to use when invoking 'removeMapping'. func makeRemoveMappingInterceptors() -> [ClientInterceptor] @@ -321,7 +321,7 @@ public enum DotLocalClientMetadata { public protocol DotLocalProvider: CallHandlerProvider { var interceptors: DotLocalServerInterceptorFactoryProtocol? { get } - func createMapping(request: CreateMappingRequest, context: StatusOnlyCallContext) -> EventLoopFuture + func createMapping(request: CreateMappingRequest, context: StatusOnlyCallContext) -> EventLoopFuture func removeMapping(request: MappingKey, context: StatusOnlyCallContext) -> EventLoopFuture @@ -344,7 +344,7 @@ extension DotLocalProvider { return UnaryServerHandler( context: context, requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), + responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeCreateMappingInterceptors() ?? [], userFunction: self.createMapping(request:context:) ) @@ -382,7 +382,7 @@ public protocol DotLocalAsyncProvider: CallHandlerProvider, Sendable { func createMapping( request: CreateMappingRequest, context: GRPCAsyncServerCallContext - ) async throws -> SwiftProtobuf.Google_Protobuf_Empty + ) async throws -> Mapping func removeMapping( request: MappingKey, @@ -418,7 +418,7 @@ extension DotLocalAsyncProvider { return GRPCAsyncServerHandler( context: context, requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), + responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeCreateMappingInterceptors() ?? [], wrapping: { try await self.createMapping(request: $0, context: $1) } ) @@ -451,7 +451,7 @@ public protocol DotLocalServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'createMapping'. /// Defaults to calling `self.makeInterceptors()`. - func makeCreateMappingInterceptors() -> [ServerInterceptor] + func makeCreateMappingInterceptors() -> [ServerInterceptor] /// - Returns: Interceptors to use when handling 'removeMapping'. /// Defaults to calling `self.makeInterceptors()`. diff --git a/DotLocal/Model/proto/dot-local.pb.swift b/Shared/Model/proto/dot-local.pb.swift similarity index 100% rename from DotLocal/Model/proto/dot-local.pb.swift rename to Shared/Model/proto/dot-local.pb.swift diff --git a/DotLocal/Model/proto/preferences.pb.swift b/Shared/Model/proto/preferences.pb.swift similarity index 100% rename from DotLocal/Model/proto/preferences.pb.swift rename to Shared/Model/proto/preferences.pb.swift diff --git a/Shared/SharedConstants.swift b/Shared/SharedConstants.swift new file mode 100644 index 0000000..68b234a --- /dev/null +++ b/Shared/SharedConstants.swift @@ -0,0 +1,104 @@ +// +// SharedConstants.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +import Foundation +import SecureXPC +import EmbeddedPropertyList + +struct SharedConstants { + static let shared = try! SharedConstants() + + /// Errors preventing shared constants from being created. + enum SharedConstantsError: Error { + /// The helper tool's launchd property list's value for `MachServices` array has no elements. + case missingMachServiceName + /// The app's info property list is missing its `SMPrivilegedExecutables` key or has no entries in the correct format. + case missingSMPrivilegedExecutables + } + + // MARK: True constants + + static let startDaemonRoute = XPCRoute.named("startDaemon") + .withMessageType(URL.self) + static let stopDaemonRoute = XPCRoute.named("stopDaemon") + static let daemonStateRoute = XPCRoute.named("daemonState") + .withSequentialReplyType(DaemonState.self) + + static let installClientRoute = XPCRoute.named("installClient") + .withMessageType(URL.self) + static let uninstallClientRoute = XPCRoute.named("uninstallClient") + + static let exitRoute = XPCRoute.named("exit") + + /// XPC route to uninstall the helper tool. + static let uninstallRoute = XPCRoute.named("uninstall") + /// XPC route to update the helper tool. + static let updateRoute = XPCRoute.named("update") + .withMessageType(URL.self) + + // MARK: Derived constants + + /// The label of the helper tool. This is required by `SMJobBless` to match its filename. + let helperToolLabel: String + /// The version of the helper tool. If this is being accessed by the helper tool this is its own version. If this is being accessed by the app it is the version of + /// the bundled helper tool. + let helperToolVersion: BundleVersion + /// The name of the Mach service registered by the helper tool. If there are multiple registered Mach services, this is set to the name of the first one. If using the + /// build script that's part of this project, then this will have the same value as `helperToolLabel`, but no such requirement exists and this code makes no + /// such assumption. + let machServiceName: String + /// Location of the helper tool's launchd property list generated by the system as part of `SMJobBless`. + /// + /// In practice this is where `SMJobBless` will place the launchd property list, but this behavior is not officially documented. + let blessedPropertyListLocation: URL + /// Location of the helper tool if it has been blessed via `SMJobBless`. + /// + /// In practice this is where `SMJobBless` will install the helper tool, but this behavior is not officially documented. + let blessedLocation: URL + + #if APP + /// Location of the helper tool within the app bundle. + let bundledLocation: URL + #endif + + /// Initializes a set of constants used throughout the app and helper tool. + init() throws { + #if APP + guard let helperToolLabel = (Bundle.main.infoDictionary?["SMPrivilegedExecutables"] + as? [String : Any])?.first?.key else { + throw SharedConstantsError.missingSMPrivilegedExecutables + } + self.helperToolLabel = helperToolLabel + self.bundledLocation = URL(fileURLWithPath: "Contents/Library/LaunchServices/\(helperToolLabel)", + relativeTo: Bundle.main.bundleURL).absoluteURL + let launchdPropertyList = try HelperToolLaunchdPropertyList(from: self.bundledLocation) + let infoPropertyList = try HelperToolInfoPropertyList(from: self.bundledLocation) + #else + #if HELPER_TOOL + let launchdPropertyList = try HelperToolLaunchdPropertyList.main + let infoPropertyList = try HelperToolInfoPropertyList.main + self.helperToolLabel = launchdPropertyList.label + #else + fatalError(""" + No Swift compiler directive was set for this executable. In this sample this is set in the AppConfig.xcconfig \ + and HelperToolConfig.xcconfig configuration files. + """) + #endif + #endif + + self.helperToolVersion = infoPropertyList.version + self.blessedPropertyListLocation = URL(fileURLWithPath: "/Library/LaunchDaemons/\(helperToolLabel).plist") + self.blessedLocation = URL(fileURLWithPath: "/Library/PrivilegedHelperTools/\(helperToolLabel)") + + // Important: If the Mach service name has been changed, for the app until that new version of the helper tool + // is installed via SMJobBless this will not result in the correct name being found. + guard let machServiceName = launchdPropertyList.machServices.first?.key else { + throw SharedConstantsError.missingMachServiceName + } + self.machServiceName = machServiceName + } +} diff --git a/dns-sd/ClientCommon.c b/dns-sd/ClientCommon.c new file mode 100644 index 0000000..f96319c --- /dev/null +++ b/dns-sd/ClientCommon.c @@ -0,0 +1,76 @@ +/* -*- Mode: C; tab-width: 4 -*- + * + * Copyright (c) 2008-2011 Apple Inc. All rights reserved. + * + * Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. + * ("Apple") in consideration of your agreement to the following terms, and your + * use, installation, modification or redistribution of this Apple software + * constitutes acceptance of these terms. If you do not agree with these terms, + * please do not use, install, modify or redistribute this Apple software. + * + * In consideration of your agreement to abide by the following terms, and subject + * to these terms, Apple grants you a personal, non-exclusive license, under Apple's + * copyrights in this original Apple software (the "Apple Software"), to use, + * reproduce, modify and redistribute the Apple Software, with or without + * modifications, in source and/or binary forms; provided that if you redistribute + * the Apple Software in its entirety and without modifications, you must retain + * this notice and the following text and disclaimers in all such redistributions of + * the Apple Software. Neither the name, trademarks, service marks or logos of + * Apple Inc. may be used to endorse or promote products derived from the + * Apple Software without specific prior written permission from Apple. Except as + * expressly stated in this notice, no other rights or licenses, express or implied, + * are granted by Apple herein, including but not limited to any patent rights that + * may be infringed by your derivative works or by other works in which the Apple + * Software may be incorporated. + * + * The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO + * WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED + * WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN + * COMBINATION WITH YOUR PRODUCTS. + * + * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION + * OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT + * (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include // For stdout, stderr + +#include "ClientCommon.h" + +const char *GetNextLabel(const char *cstr, char label[64]) +{ + char *ptr = label; + while (*cstr && *cstr != '.') // While we have characters in the label... + { + char c = *cstr++; + if (c == '\\') // If escape character, check next character + { + if (*cstr == '\0') break; // If this is the end of the string, then break + c = *cstr++; + if (isdigit(cstr[-1]) && isdigit(cstr[0]) && isdigit(cstr[1])) + { + int v0 = cstr[-1] - '0'; // then interpret as three-digit decimal + int v1 = cstr[ 0] - '0'; + int v2 = cstr[ 1] - '0'; + int val = v0 * 100 + v1 * 10 + v2; + // If valid three-digit decimal value, use it + // Note that although ascii nuls are possible in DNS labels + // we're building a C string here so we have no way to represent that + if (val == 0) val = '-'; + if (val <= 255) { c = (char)val; cstr += 2; } + } + } + *ptr++ = c; + if (ptr >= label+64) { label[63] = 0; return(NULL); } // Illegal label more than 63 bytes + } + *ptr = 0; // Null-terminate label text + if (ptr == label) return(NULL); // Illegal empty label + if (*cstr) cstr++; // Skip over the trailing dot (if present) + return(cstr); +} diff --git a/dns-sd/ClientCommon.h b/dns-sd/ClientCommon.h new file mode 100644 index 0000000..afe5b7a --- /dev/null +++ b/dns-sd/ClientCommon.h @@ -0,0 +1,41 @@ +/* -*- Mode: C; tab-width: 4 -*- + * + * Copyright (c) 2008 Apple Inc. All rights reserved. + * + * Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Computer, Inc. + * ("Apple") in consideration of your agreement to the following terms, and your + * use, installation, modification or redistribution of this Apple software + * constitutes acceptance of these terms. If you do not agree with these terms, + * please do not use, install, modify or redistribute this Apple software. + * + * In consideration of your agreement to abide by the following terms, and subject + * to these terms, Apple grants you a personal, non-exclusive license, under Apple's + * copyrights in this original Apple software (the "Apple Software"), to use, + * reproduce, modify and redistribute the Apple Software, with or without + * modifications, in source and/or binary forms; provided that if you redistribute + * the Apple Software in its entirety and without modifications, you must retain + * this notice and the following text and disclaimers in all such redistributions of + * the Apple Software. Neither the name, trademarks, service marks or logos of + * Apple Computer, Inc. may be used to endorse or promote products derived from the + * Apple Software without specific prior written permission from Apple. Except as + * expressly stated in this notice, no other rights or licenses, express or implied, + * are granted by Apple herein, including but not limited to any patent rights that + * may be infringed by your derivative works or by other works in which the Apple + * Software may be incorporated. + * + * The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO + * WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED + * WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN + * COMBINATION WITH YOUR PRODUCTS. + * + * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION + * OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT + * (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +extern const char *GetNextLabel(const char *cstr, char label[64]); diff --git a/dns-sd/Makefile b/dns-sd/Makefile new file mode 100644 index 0000000..b8f7f9b --- /dev/null +++ b/dns-sd/Makefile @@ -0,0 +1,61 @@ +# -*- tab-width: 4 -*- +# +# Copyright (c) 2002-2004, 2015 Apple Computer, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Notes: +# $@ means "The file name of the target of the rule" +# $< means "The name of the first prerequisite" +# $+ means "The names of all the prerequisites, with spaces between them, exactly as given" +# For more magic automatic variables, see +# +# +# This makefile uses $(CC) for compilation and linking, which is +# an automatic implicit gcc variable that defaults to "cc" +# + +############################################################################# + +# On OS X the dns_sd library functions are included in libSystem, which is implicitly linked with every executable +# If /usr/lib/libSystem.dylib exists, then we're on OS X, so we don't need also to link the "dns_sd" shared library +ifeq "$(DEBUG)" "1" +DEBUGFLAGS = -g +BUILDDIR = build/debug +else +DEBUGFLAGS = -Os +BUILDDIR = build/prod +endif + +TARGETS = build/dns-sd build/dns-sd64 +LIBS = + +all: $(TARGETS) + +clean: + rm -rf build + +build: + mkdir build + +build/dns-sd: build dns-sd.c ClientCommon.c + $(CC) $(SUPMAKE_CFLAGS) $(filter %.c %.o, $+) $(LIBS) -I./mDNSShared -Wall -o $@ + +build/dns-sd64: build dns-sd.c ClientCommon.c + $(CC) $(SUPMAKE_CFLAGS) $(filter %.c %.o, $+) $(LIBS) -I./mDNSShared -Wall -o $@ -m64 + +# Note, we can make a 'fat' version of dns-sd using 'lipo', as shown below, but we +# don't, because we don't want or need a 'fat' version of dns-sd, because it will +# never need to access more than 4GB of data. We build the 64-bit version purely so +# we have a test tool for making sure that the APIs work properly from 64-bit clients. +# lipo -create dns-sd dns-sd64 -output dns-sd-fat diff --git a/dns-sd/dns-sd.c b/dns-sd/dns-sd.c new file mode 100644 index 0000000..fa52596 --- /dev/null +++ b/dns-sd/dns-sd.c @@ -0,0 +1,361 @@ +#include +#include // For stdout, stderr +#include // For exit() +#include // For strlen(), strcpy() +#include // For va_start, va_arg, va_end, etc. +#include // For errno, EINTR +#include +#include // For u_char +#include +#include + + + +#ifndef __printflike + #define __printflike(A, B) +#endif + +#ifdef _WIN32 + #include + #include + #include + #include + #include +typedef int pid_t; +typedef int suseconds_t; + #define getpid _getpid + #define strcasecmp _stricmp + #define snprintf _snprintf +static const char kFilePathSep = '\\'; + #ifndef HeapEnableTerminationOnCorruption + # define HeapEnableTerminationOnCorruption (HEAP_INFORMATION_CLASS)1 + #endif + #if !defined(IFNAMSIZ) + #define IFNAMSIZ 16 + #endif + #define if_nametoindex if_nametoindex_win + #define if_indextoname if_indextoname_win + +typedef PCHAR (WINAPI * if_indextoname_funcptr_t)(ULONG index, PCHAR name); +typedef ULONG (WINAPI * if_nametoindex_funcptr_t)(PCSTR name); + +unsigned if_nametoindex_win(const char *ifname) +{ + HMODULE library; + unsigned index = 0; + + // Try and load the IP helper library dll + if ((library = LoadLibrary(TEXT("Iphlpapi")) ) != NULL ) + { + if_nametoindex_funcptr_t if_nametoindex_funcptr; + + // On Vista and above there is a Posix like implementation of if_nametoindex + if ((if_nametoindex_funcptr = (if_nametoindex_funcptr_t) GetProcAddress(library, "if_nametoindex")) != NULL ) + { + index = if_nametoindex_funcptr(ifname); + } + + FreeLibrary(library); + } + + return index; +} + +char * if_indextoname_win( unsigned ifindex, char *ifname) +{ + HMODULE library; + char * name = NULL; + + // Try and load the IP helper library dll + if ((library = LoadLibrary(TEXT("Iphlpapi")) ) != NULL ) + { + if_indextoname_funcptr_t if_indextoname_funcptr; + + // On Vista and above there is a Posix like implementation of if_indextoname + if ((if_indextoname_funcptr = (if_indextoname_funcptr_t) GetProcAddress(library, "if_indextoname")) != NULL ) + { + name = if_indextoname_funcptr(ifindex, ifname); + } + + FreeLibrary(library); + } + + return name; +} + +static size_t _sa_len(const struct sockaddr *addr) +{ + if (addr->sa_family == AF_INET) return (sizeof(struct sockaddr_in)); + else if (addr->sa_family == AF_INET6) return (sizeof(struct sockaddr_in6)); + else return (sizeof(struct sockaddr)); +} + +# define SA_LEN(addr) (_sa_len(addr)) + +typedef void (WINAPI* SystemTimeFunc)(LPFILETIME); + +static const uint64_t epoch_diff = (UINT64)11644473600000000ULL; +static SystemTimeFunc fpTimeFunc; + +int gettimeofday(struct timeval* tp, struct timezone* tzp) +{ + FILETIME ft; + UINT64 us; + + if (!fpTimeFunc) + { + /* available on Windows 7 */ + fpTimeFunc = GetSystemTimeAsFileTime; + + HMODULE hKernel32 = LoadLibraryW(L"kernel32.dll"); + if (hKernel32) + { + FARPROC fp; + + /* available on Windows 8+ */ + fp = GetProcAddress(hKernel32, "GetSystemTimePreciseAsFileTime"); + if (fp) + { + fpTimeFunc = (SystemTimeFunc)fp; + } + } + } + + fpTimeFunc(&ft); + + us = (((uint64_t)ft.dwHighDateTime << 32) | (uint64_t)ft.dwLowDateTime) / 10; + us -= epoch_diff; + + tp->tv_sec = (long)(us / 1000000); + tp->tv_usec = (long)(us % 1000000); + + return 0; +} + +#else + #include // For getopt() and optind + #include // For getaddrinfo() + #include // For struct timeval + #include // For AF_INET + #include // For struct sockaddr_in() + #include // For inet_addr() + #include // For if_nametoindex() +static const char kFilePathSep = '/'; +// #ifndef NOT_HAVE_SA_LEN +// #define SA_LEN(addr) ((addr)->sa_len) +// #else + #define SA_LEN(addr) (((addr)->sa_family == AF_INET6) ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)) +// #endif +#endif + +#if (TEST_NEW_CLIENTSTUB && !defined(__APPLE_API_PRIVATE)) +#define __APPLE_API_PRIVATE 1 +#endif + +// DNSServiceSetDispatchQueue is not supported on 10.6 & prior +#if !TEST_NEW_CLIENTSTUB && defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ - (__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ % 10) <= 1060) +#undef _DNS_SD_LIBDISPATCH +#endif +#include "dns_sd.h" +#include "ClientCommon.h" + + +#if TEST_NEW_CLIENTSTUB +#include "../mDNSShared/dnssd_ipc.c" +#include "../mDNSShared/dnssd_clientlib.c" +#include "../mDNSShared/dnssd_clientstub.c" +#endif + +#include "dns-sd.h" + +/** + * Global +*/ + +#if _DNS_SD_LIBDISPATCH +static dispatch_queue_t main_queue; +static dispatch_source_t timer_source; +#endif + +#if _DNS_SD_LIBDISPATCH +#define EXIT_IF_LIBDISPATCH_FATAL_ERROR(E) \ + if (main_queue && (E) == kDNSServiceErr_ServiceNotRunning) { fprintf(stderr, "Error code %d\n", (E)); exit(0); } +#else +#define EXIT_IF_LIBDISPATCH_FATAL_ERROR(E) +#endif + +static int exitWhenNoMoreComing; + +#define printtimestamp() printtimestamp_F(stdout) + +static void printtimestamp_F(FILE *outstream) +{ + struct tm tm; + int ms; + static char date[16]; + static char new_date[16]; +#ifdef _WIN32 + SYSTEMTIME sysTime; + time_t uct = time(NULL); + tm = *localtime(&uct); + GetLocalTime(&sysTime); + ms = sysTime.wMilliseconds; +#else + struct timeval tv; + gettimeofday(&tv, NULL); + localtime_r((time_t*)&tv.tv_sec, &tm); + ms = tv.tv_usec/1000; +#endif + strftime(new_date, sizeof(new_date), "%a %d %b %Y", &tm); + if (strncmp(date, new_date, sizeof(new_date))) + { + fprintf(outstream, "DATE: ---%s---\n", new_date); //display date only if it has changed + strncpy(date, new_date, sizeof(date)); + } + fprintf(outstream, "%2d:%02d:%02d.%03d ", tm.tm_hour, tm.tm_min, tm.tm_sec, ms); +} + +static void DNSSD_API MyRegisterRecordCallback(DNSServiceRef service, DNSRecordRef rec, const DNSServiceFlags flags, + DNSServiceErrorType errorCode, void *context) +{ + char *name = (char *)context; + + (void)service; // Unused + (void)rec; // Unused + (void)flags; // Unused + EXIT_IF_LIBDISPATCH_FATAL_ERROR(errorCode); + + printtimestamp(); + printf("Got a reply for record %s: ", name); + + switch (errorCode) + { + case kDNSServiceErr_NoError: printf("Name now registered and active\n"); break; + case kDNSServiceErr_NameConflict: printf("Name in use, please choose another\n"); exit(-1); + default: printf("Error %d\n", errorCode); break; + } + if (!(flags & kDNSServiceFlagsMoreComing)) + { + fflush(stdout); + if (exitWhenNoMoreComing) exit(0); + } +} + + +static void getip(const char *const name, struct sockaddr_storage *result) +{ + struct addrinfo *addrs = NULL; + int err = getaddrinfo(name, NULL, NULL, &addrs); + if (err) fprintf(stderr, "getaddrinfo error %d for %s", err, name); + else memcpy(result, addrs->ai_addr, SA_LEN(addrs->ai_addr)); + if (addrs) freeaddrinfo(addrs); +} + +DNSServiceErrorType RegisterProxyAddressRecord(DNSServiceRef sdref, DNSRecordRef *RecordRef, const char *host, const char *ip, DNSServiceFlags flags) +{ + // Call getip() after the call DNSServiceCreateConnection(). + // On the Win32 platform, WinSock must be initialized for getip() to succeed. + // Any DNSService* call will initialize WinSock for us, so we make sure + // DNSServiceCreateConnection() is called before getip() is. + struct sockaddr_storage hostaddr; + memset(&hostaddr, 0, sizeof(hostaddr)); + getip(ip, &hostaddr); + if (!(flags & kDNSServiceFlagsShared)) + { + flags |= kDNSServiceFlagsUnique; + } + if (hostaddr.ss_family == AF_INET) + return(DNSServiceRegisterRecord(sdref, RecordRef, flags, kDNSServiceInterfaceIndexLocalOnly, host, + kDNSServiceType_A, kDNSServiceClass_IN, 4, &((struct sockaddr_in *)&hostaddr)->sin_addr, 240, MyRegisterRecordCallback, (void*)host)); + else if (hostaddr.ss_family == AF_INET6) + return(DNSServiceRegisterRecord(sdref, RecordRef, flags, kDNSServiceInterfaceIndexLocalOnly, host, + kDNSServiceType_AAAA, kDNSServiceClass_IN, 16, &((struct sockaddr_in6*)&hostaddr)->sin6_addr, 240, MyRegisterRecordCallback, (void*)host)); + else return(kDNSServiceErr_BadParam); +} + +static DNSServiceRef client_pa = NULL; +static int exitTimeout; + + +static void HandleEvents(void) +#if _DNS_SD_LIBDISPATCH +{ + main_queue = dispatch_get_main_queue(); + if (client_pa) DNSServiceSetDispatchQueue(client_pa, main_queue); + dispatch_main(); +} +#else +{ + int dns_sd_fd = client ? DNSServiceRefSockFD(client ) : -1; + int dns_sd_fd2 = client_pa ? DNSServiceRefSockFD(client_pa) : -1; + int nfds = dns_sd_fd + 1; + fd_set readfds; + struct timeval tv; + int result; + uint64_t timeout_when, now; + int expectingMyTimer; + + if (dns_sd_fd2 > dns_sd_fd) nfds = dns_sd_fd2 + 1; + + if (exitTimeout != 0) { + gettimeofday(&tv, NULL); + timeout_when = tv.tv_sec * 1000ULL * 1000ULL + tv.tv_usec + exitTimeout * 1000ULL * 1000ULL; + } + + while (!stopNow) + { + // 1. Set up the fd_set as usual here. + // This example client has no file descriptors of its own, + // but a real application would call FD_SET to add them to the set here + FD_ZERO(&readfds); + + // 2. Add the fd for our client(s) to the fd_set + if (client ) FD_SET(dns_sd_fd, &readfds); + if (client_pa) FD_SET(dns_sd_fd2, &readfds); + + // 3. Set up the timeout. + expectingMyTimer = 1; + if (exitTimeout > 0) { + gettimeofday(&tv, NULL); + now = tv.tv_sec * 1000ULL * 1000ULL + tv.tv_usec; + if (timeout_when <= now) { + exit(0); + } + if (timeout_when - now < timeOut * 1000ULL * 1000ULL) { + tv.tv_sec = (time_t)(timeout_when - now) / 1000 / 1000; + tv.tv_usec = (suseconds_t)(timeout_when % (1000 * 1000)); + expectingMyTimer = 0; + } + } + if (expectingMyTimer) { + tv.tv_sec = timeOut; + tv.tv_usec = 0; + } + result = select(nfds, &readfds, (fd_set*)NULL, (fd_set*)NULL, &tv); + if (result > 0) + { + DNSServiceErrorType err = kDNSServiceErr_NoError; + if (client && FD_ISSET(dns_sd_fd, &readfds)) err = DNSServiceProcessResult(client ); + else if (client_pa && FD_ISSET(dns_sd_fd2, &readfds)) err = DNSServiceProcessResult(client_pa); + if (err) { printtimestamp_F(stderr); fprintf(stderr, "DNSServiceProcessResult returned %d\n", err); stopNow = 1; } + } + else if (result == 0) + { + if (expectingMyTimer) + { + myTimerCallBack(); + } + else + { + // exitTimeout has elapsed. + exit(0); + } + } + else + { + printf("select() returned %d errno %d %s\n", result, errno, strerror(errno)); + if (errno != EINTR) stopNow = 1; + } + } +} +#endif diff --git a/dns-sd/dns-sd.h b/dns-sd/dns-sd.h new file mode 100644 index 0000000..18a2c51 --- /dev/null +++ b/dns-sd/dns-sd.h @@ -0,0 +1,8 @@ +#ifndef _DNSSD_H +#define _DNSSD_H + +#include "dns_sd.h" + +DNSServiceErrorType RegisterProxyAddressRecord(DNSServiceRef sdref, DNSRecordRef *RecordRef, const char *host, const char *ip, DNSServiceFlags flags); + +#endif diff --git a/dns-sd/enums.go b/dns-sd/enums.go new file mode 100644 index 0000000..2b36d7f --- /dev/null +++ b/dns-sd/enums.go @@ -0,0 +1,43 @@ +package dnssd + +// #cgo CFLAGS: -g -Wall +// #include "dns-sd.h" +import "C" + +const ( + kDNSServiceErr_NoError = C.kDNSServiceErr_NoError + kDNSServiceErr_Unknown = C.kDNSServiceErr_Unknown + kDNSServiceErr_NoSuchName = C.kDNSServiceErr_NoSuchName + kDNSServiceErr_NoMemory = C.kDNSServiceErr_NoMemory + kDNSServiceErr_BadParam = C.kDNSServiceErr_BadParam + kDNSServiceErr_BadReference = C.kDNSServiceErr_BadReference + kDNSServiceErr_BadState = C.kDNSServiceErr_BadState + kDNSServiceErr_BadFlags = C.kDNSServiceErr_BadFlags + kDNSServiceErr_Unsupported = C.kDNSServiceErr_Unsupported + kDNSServiceErr_NotInitialized = C.kDNSServiceErr_NotInitialized + kDNSServiceErr_AlreadyRegistered = C.kDNSServiceErr_AlreadyRegistered + kDNSServiceErr_NameConflict = C.kDNSServiceErr_NameConflict + kDNSServiceErr_Invalid = C.kDNSServiceErr_Invalid + kDNSServiceErr_Firewall = C.kDNSServiceErr_Firewall + kDNSServiceErr_Incompatible = C.kDNSServiceErr_Incompatible + kDNSServiceErr_BadInterfaceIndex = C.kDNSServiceErr_BadInterfaceIndex + kDNSServiceErr_Refused = C.kDNSServiceErr_Refused + kDNSServiceErr_NoSuchRecord = C.kDNSServiceErr_NoSuchRecord + kDNSServiceErr_NoAuth = C.kDNSServiceErr_NoAuth + kDNSServiceErr_NoSuchKey = C.kDNSServiceErr_NoSuchKey + kDNSServiceErr_NATTraversal = C.kDNSServiceErr_NATTraversal + kDNSServiceErr_DoubleNAT = C.kDNSServiceErr_DoubleNAT + kDNSServiceErr_BadTime = C.kDNSServiceErr_BadTime + kDNSServiceErr_BadSig = C.kDNSServiceErr_BadSig + kDNSServiceErr_BadKey = C.kDNSServiceErr_BadKey + kDNSServiceErr_Transient = C.kDNSServiceErr_Transient + kDNSServiceErr_ServiceNotRunning = C.kDNSServiceErr_ServiceNotRunning + kDNSServiceErr_NATPortMappingUnsupported = C.kDNSServiceErr_NATPortMappingUnsupported + kDNSServiceErr_NATPortMappingDisabled = C.kDNSServiceErr_NATPortMappingDisabled + kDNSServiceErr_NoRouter = C.kDNSServiceErr_NoRouter + kDNSServiceErr_PollingMode = C.kDNSServiceErr_PollingMode + kDNSServiceErr_Timeout = C.kDNSServiceErr_Timeout + kDNSServiceErr_DefunctConnection = C.kDNSServiceErr_DefunctConnection + kDNSServiceErr_PolicyDenied = C.kDNSServiceErr_PolicyDenied + kDNSServiceErr_NotPermitted = C.kDNSServiceErr_NotPermitted +) diff --git a/dns-sd/errors.go b/dns-sd/errors.go new file mode 100644 index 0000000..6ca249c --- /dev/null +++ b/dns-sd/errors.go @@ -0,0 +1,18 @@ +package dnssd + +// #cgo CFLAGS: -g -Wall +// #include "dns-sd.h" +import "C" +import "fmt" + +type DNSServiceError struct { + Code C.DNSServiceErrorType +} + +func NewDNSServiceError(code C.DNSServiceErrorType) *DNSServiceError { + return &DNSServiceError{code} +} + +func (m *DNSServiceError) Error() string { + return fmt.Sprintf("DNSServiceError %d", m.Code) +} diff --git a/dns-sd/record.go b/dns-sd/record.go new file mode 100644 index 0000000..3ca41c9 --- /dev/null +++ b/dns-sd/record.go @@ -0,0 +1,21 @@ +package dnssd + +// #cgo CFLAGS: -g -Wall +// #include "dns-sd.h" +import "C" + +type DNSRecord interface { + ref() C.DNSRecordRef + + implementsOpaque() +} + +type dnsRecord struct { + _ref C.DNSRecordRef +} + +func (r *dnsRecord) ref() C.DNSRecordRef { + return r._ref +} + +func (r *dnsRecord) implementsOpaque() {} diff --git a/dns-sd/service.go b/dns-sd/service.go new file mode 100644 index 0000000..2c3684c --- /dev/null +++ b/dns-sd/service.go @@ -0,0 +1,119 @@ +package dnssd + +// #cgo CFLAGS: -g -Wall +// #include "dns-sd.h" +import "C" +import ( + "context" + "fmt" + "os" + + "github.com/samber/lo" + "golang.org/x/sys/unix" +) + +type DNSService interface { + RegisterProxyAddressRecord(host string, ip string, flags C.DNSServiceFlags) (DNSRecord, error) + RemoveRecord(record DNSRecord, flags C.DNSServiceFlags) error + Process(ctx context.Context) error + Deallocate() + + implementsOpaque() +} + +type dnsService struct { + ref C.DNSServiceRef +} + +func NewConnection() (DNSService, error) { + var service dnsService + res := C.DNSServiceCreateConnection(&service.ref) + if res != C.kDNSServiceErr_NoError { + return nil, NewDNSServiceError(res) + } + return &service, nil +} + +func (s *dnsService) RegisterProxyAddressRecord(host string, ip string, flags C.DNSServiceFlags) (DNSRecord, error) { + var record dnsRecord + res := C.RegisterProxyAddressRecord(s.ref, &record._ref, C.CString(host), C.CString(ip), C.uint32_t(flags)) + if res != C.kDNSServiceErr_NoError { + return nil, NewDNSServiceError(res) + } + return &record, nil +} + +func (s *dnsService) RemoveRecord(record DNSRecord, flags C.DNSServiceFlags) error { + res := C.DNSServiceRemoveRecord(s.ref, record.ref(), flags) + if res != C.kDNSServiceErr_NoError { + return NewDNSServiceError(res) + } + return nil +} + +func (s *dnsService) Process(ctx context.Context) error { + socket := s.useNonblockingSocket() + + fd := os.NewFile(uintptr(socket), "dnssd") + defer fd.Close() + + for { + if isContextDone(ctx) { + return nil + } + + readSet := unix.FdSet{} + readSet.Zero() + readSet.Set(int(socket)) + + result, err := unix.Select(int(fd.Fd())+1, &readSet, nil, nil, &unix.Timeval{Sec: 10}) + if err != nil { + if err == unix.EINTR { + continue + } + if isContextDone(ctx) { + return nil + } + return err + } + if result > 0 { + res := C.DNSServiceProcessResult(s.ref) + if res != C.kDNSServiceErr_NoError { + return NewDNSServiceError(res) + } + } else if result == 0 { + continue + } else { + panic(fmt.Sprintf("select error: %d", result)) + } + } +} + +func (s *dnsService) Deallocate() { + C.DNSServiceRefDeallocate(s.ref) +} + +func (s *dnsService) socket() C.dnssd_sock_t { + return C.DNSServiceRefSockFD(s.ref) +} + +func (s *dnsService) useNonblockingSocket() C.dnssd_sock_t { + socket := s.socket() + flags := lo.Must1(unix.FcntlInt(uintptr(socket), unix.F_GETFL, 0)) + if flags == -1 { + flags = 0 + } + _ = lo.Must1(unix.FcntlInt(uintptr(socket), unix.F_SETFL, flags|unix.O_NONBLOCK)) + return socket +} + +func (s *dnsService) implementsOpaque() {} + +func isContextDone(ctx context.Context) bool { + select { + case <-ctx.Done(): + return true + default: + return false + } +} diff --git a/go.mod b/go.mod index dd67718..f284958 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.21.5 require ( github.com/dchest/uniuri v1.2.0 - github.com/docker/docker v24.0.7+incompatible github.com/samber/lo v1.39.0 + github.com/spf13/cobra v1.8.0 github.com/tufanbarisyildirim/gonginx v0.0.0-20231222202608-ba16e88a9436 go.uber.org/zap v1.26.0 google.golang.org/grpc v1.60.1 @@ -14,30 +14,15 @@ require ( ) require ( - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/moby/term v0.5.0 // indirect - github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.4 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.16.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.6.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index cac3958..4a66dad 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,8 @@ -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -27,18 +11,6 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -52,55 +24,21 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tufanbarisyildirim/gonginx v0.0.0-20231222202608-ba16e88a9436 h1:i9TLbw23bUawnhimf5SghqkLrDRdpa65vw0hUqYhCB0= github.com/tufanbarisyildirim/gonginx v0.0.0-20231222202608-ba16e88a9436/go.mod h1:4fTjBxMoWGOIVnGFSTS9GAZ0yMyiGzTdATQS0krQv18= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= diff --git a/internal/api/proto/dot-local.pb.go b/internal/api/proto/dot-local.pb.go index cfec7df..1dfe970 100644 --- a/internal/api/proto/dot-local.pb.go +++ b/internal/api/proto/dot-local.pb.go @@ -299,21 +299,20 @@ var file_proto_dot_local_proto_rawDesc = []byte{ 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x08, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x32, - 0xc5, 0x01, 0x0a, 0x08, 0x44, 0x6f, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x12, 0x40, 0x0a, 0x0d, + 0xb7, 0x01, 0x0a, 0x08, 0x44, 0x6f, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x12, 0x32, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x15, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x36, - 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, - 0x0b, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x1a, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3f, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x15, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6f, 0x66, 0x74, 0x6e, 0x65, 0x74, 0x69, 0x63, 0x73, - 0x2f, 0x64, 0x6f, 0x74, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x08, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0x00, + 0x12, 0x36, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, + 0x67, 0x12, 0x0b, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x4b, 0x65, 0x79, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3f, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, + 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x15, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6f, 0x66, 0x74, 0x6e, 0x65, 0x74, 0x69, + 0x63, 0x73, 0x2f, 0x64, 0x6f, 0x74, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, } var ( @@ -343,7 +342,7 @@ var file_proto_dot_local_proto_depIdxs = []int32{ 0, // 2: DotLocal.CreateMapping:input_type -> CreateMappingRequest 1, // 3: DotLocal.RemoveMapping:input_type -> MappingKey 5, // 4: DotLocal.ListMappings:input_type -> google.protobuf.Empty - 5, // 5: DotLocal.CreateMapping:output_type -> google.protobuf.Empty + 2, // 5: DotLocal.CreateMapping:output_type -> Mapping 5, // 6: DotLocal.RemoveMapping:output_type -> google.protobuf.Empty 3, // 7: DotLocal.ListMappings:output_type -> ListMappingsResponse 5, // [5:8] is the sub-list for method output_type diff --git a/internal/api/proto/dot-local_grpc.pb.go b/internal/api/proto/dot-local_grpc.pb.go index b11d67e..73bcce9 100644 --- a/internal/api/proto/dot-local_grpc.pb.go +++ b/internal/api/proto/dot-local_grpc.pb.go @@ -23,7 +23,7 @@ const _ = grpc.SupportPackageIsVersion7 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type DotLocalClient interface { - CreateMapping(ctx context.Context, in *CreateMappingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + CreateMapping(ctx context.Context, in *CreateMappingRequest, opts ...grpc.CallOption) (*Mapping, error) RemoveMapping(ctx context.Context, in *MappingKey, opts ...grpc.CallOption) (*emptypb.Empty, error) ListMappings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMappingsResponse, error) } @@ -36,8 +36,8 @@ func NewDotLocalClient(cc grpc.ClientConnInterface) DotLocalClient { return &dotLocalClient{cc} } -func (c *dotLocalClient) CreateMapping(ctx context.Context, in *CreateMappingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { - out := new(emptypb.Empty) +func (c *dotLocalClient) CreateMapping(ctx context.Context, in *CreateMappingRequest, opts ...grpc.CallOption) (*Mapping, error) { + out := new(Mapping) err := c.cc.Invoke(ctx, "/DotLocal/CreateMapping", in, out, opts...) if err != nil { return nil, err @@ -67,7 +67,7 @@ func (c *dotLocalClient) ListMappings(ctx context.Context, in *emptypb.Empty, op // All implementations must embed UnimplementedDotLocalServer // for forward compatibility type DotLocalServer interface { - CreateMapping(context.Context, *CreateMappingRequest) (*emptypb.Empty, error) + CreateMapping(context.Context, *CreateMappingRequest) (*Mapping, error) RemoveMapping(context.Context, *MappingKey) (*emptypb.Empty, error) ListMappings(context.Context, *emptypb.Empty) (*ListMappingsResponse, error) mustEmbedUnimplementedDotLocalServer() @@ -77,7 +77,7 @@ type DotLocalServer interface { type UnimplementedDotLocalServer struct { } -func (UnimplementedDotLocalServer) CreateMapping(context.Context, *CreateMappingRequest) (*emptypb.Empty, error) { +func (UnimplementedDotLocalServer) CreateMapping(context.Context, *CreateMappingRequest) (*Mapping, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateMapping not implemented") } func (UnimplementedDotLocalServer) RemoveMapping(context.Context, *MappingKey) (*emptypb.Empty, error) { diff --git a/internal/client/cmd/root.go b/internal/client/cmd/root.go index 25f54a6..72d0339 100644 --- a/internal/client/cmd/root.go +++ b/internal/client/cmd/root.go @@ -38,12 +38,14 @@ var ( exitCode := 0 + var createdMapping *api.Mapping + loopCtx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { wasSuccessful := false for { - _, err := apiClient.CreateMapping(loopCtx, &api.CreateMappingRequest{ + mapping, err := apiClient.CreateMapping(loopCtx, &api.CreateMappingRequest{ Host: &hostname, PathPrefix: &pathPrefix, Target: &target, @@ -54,9 +56,10 @@ var ( duration = 5 * time.Second wasSuccessful = false } else if !wasSuccessful { - logger.Info(fmt.Sprintf("Forwarding %s%s to %s", hostname, pathPrefix, target)) + logger.Info(fmt.Sprintf("Forwarding http://%s%s to %s", *mapping.Host, *mapping.PathPrefix, *mapping.Target)) wasSuccessful = true } + createdMapping = mapping timer := time.NewTimer(duration) select { case <-timer.C: @@ -109,12 +112,14 @@ var ( <-ch } - _, err = apiClient.RemoveMapping(context.Background(), &api.MappingKey{ - Host: &hostname, - PathPrefix: &pathPrefix, - }) - if err != nil { - log.Fatal(err) + if createdMapping != nil { + _, err = apiClient.RemoveMapping(context.Background(), &api.MappingKey{ + Host: createdMapping.Host, + PathPrefix: createdMapping.PathPrefix, + }) + if err != nil { + log.Fatal(err) + } } os.Exit(exitCode) }, diff --git a/internal/daemon/apiserver.go b/internal/daemon/apiserver.go index 24a8c58..1273937 100644 --- a/internal/daemon/apiserver.go +++ b/internal/daemon/apiserver.go @@ -31,20 +31,27 @@ func NewAPIServer(logger *zap.Logger, dotlocal *DotLocal) (*APIServer, error) { }, nil } -func (s *APIServer) Start() error { - err := s.killExistingProcess() +func (s *APIServer) Start(ctx context.Context) error { + err := s.killExistingProcessIfNeeded() if err != nil { return err } pid := os.Getpid() err = os.WriteFile(util.GetPidPath(), []byte(strconv.Itoa(pid)), 0644) + if err != nil { + return err + } socketPath := util.GetApiSocketPath() lis, err := net.Listen("unix", socketPath) if err != nil { return err } + err = os.Chmod(socketPath, 0666) + if err != nil { + return err + } var opts []grpc.ServerOption s.grpcServer = grpc.NewServer(opts...) api.RegisterDotLocalServer(s.grpcServer, newDotLocalServer(s.logger, s.dotlocal)) @@ -68,7 +75,7 @@ func (s *APIServer) Stop() error { return nil } -func (s *APIServer) killExistingProcess() error { +func (s *APIServer) killExistingProcessIfNeeded() error { _, err := os.Stat(util.GetApiSocketPath()) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -78,8 +85,17 @@ func (s *APIServer) killExistingProcess() error { } s.logger.Info("Killing existing process", zap.String("path", util.GetApiSocketPath())) + _ = killExistingProcess() + + _ = os.Remove(util.GetPidPath()) + _ = os.Remove(util.GetApiSocketPath()) + + return nil +} + +func killExistingProcess() error { pidBytes, err := os.ReadFile(util.GetPidPath()) - if err != nil { + if err == nil { return err } pid, err := strconv.Atoi(string(pidBytes)) @@ -91,17 +107,10 @@ func (s *APIServer) killExistingProcess() error { if err != nil { return err } - _ = process.Kill() - - err = os.Remove(util.GetPidPath()) - if err != nil { - return err - } - err = os.Remove(util.GetApiSocketPath()) + err = process.Kill() if err != nil { return err } - return nil } @@ -119,8 +128,8 @@ func newDotLocalServer(logger *zap.Logger, dotlocal *DotLocal) *dotLocalServer { } } -func (s *dotLocalServer) CreateMapping(ctx context.Context, req *api.CreateMappingRequest) (*emptypb.Empty, error) { - _, err := s.dotlocal.CreateMapping(internal.MappingOptions{ +func (s *dotLocalServer) CreateMapping(ctx context.Context, req *api.CreateMappingRequest) (*api.Mapping, error) { + mapping, err := s.dotlocal.CreateMapping(internal.MappingOptions{ Host: *req.Host, PathPrefix: *req.PathPrefix, Target: *req.Target, @@ -128,7 +137,7 @@ func (s *dotLocalServer) CreateMapping(ctx context.Context, req *api.CreateMappi if err != nil { return nil, err } - return &emptypb.Empty{}, nil + return mappingToApiMapping(mapping), nil } func (s *dotLocalServer) RemoveMapping(ctx context.Context, key *api.MappingKey) (*emptypb.Empty, error) { @@ -145,16 +154,20 @@ func (s *dotLocalServer) RemoveMapping(ctx context.Context, key *api.MappingKey) func (s *dotLocalServer) ListMappings(ctx context.Context, _ *emptypb.Empty) (*api.ListMappingsResponse, error) { res := &api.ListMappingsResponse{ Mappings: lo.Map(s.dotlocal.GetMappings(), func(mapping internal.Mapping, _ int) *api.Mapping { - return &api.Mapping{ - Id: &mapping.ID, - Host: &mapping.Host, - PathPrefix: &mapping.PathPrefix, - Target: &mapping.Target, - ExpiresAt: ×tamppb.Timestamp{ - Seconds: mapping.ExpresAt.Unix(), - }, - } + return mappingToApiMapping(mapping) }), } return res, nil } + +func mappingToApiMapping(mapping internal.Mapping) *api.Mapping { + return &api.Mapping{ + Id: &mapping.ID, + Host: &mapping.Host, + PathPrefix: &mapping.PathPrefix, + Target: &mapping.Target, + ExpiresAt: ×tamppb.Timestamp{ + Seconds: mapping.ExpresAt.Unix(), + }, + } +} diff --git a/internal/daemon/dnsproxy/interface.go b/internal/daemon/dnsproxy/interface.go index 4b10f44..04f4720 100644 --- a/internal/daemon/dnsproxy/interface.go +++ b/internal/daemon/dnsproxy/interface.go @@ -1,7 +1,9 @@ package dnsproxy +import "context" + type DNSProxy interface { - Start(port int) error + Start(ctx context.Context) error SetHosts(hosts map[string]struct{}) error Stop() error } diff --git a/internal/daemon/dotlocal.go b/internal/daemon/dotlocal.go index ef6ad28..844b8d9 100644 --- a/internal/daemon/dotlocal.go +++ b/internal/daemon/dotlocal.go @@ -4,6 +4,7 @@ import ( "context" "errors" "os" + "strings" "time" "github.com/dchest/uniuri" @@ -11,7 +12,7 @@ import ( "github.com/softnetics/dotlocal/internal" api "github.com/softnetics/dotlocal/internal/api/proto" "github.com/softnetics/dotlocal/internal/daemon/dnsproxy" - "github.com/softnetics/dotlocal/internal/daemon/orbdnsproxy" + "github.com/softnetics/dotlocal/internal/daemon/mdnsproxy" "github.com/softnetics/dotlocal/internal/util" "go.uber.org/zap" "google.golang.org/protobuf/encoding/protojson" @@ -34,7 +35,7 @@ func NewDotLocal(logger *zap.Logger) (*DotLocal, error) { return nil, err } - dnsProxy, err := orbdnsproxy.NewOrbstackDNSProxy(logger.Named("orbdnsproxy")) + dnsProxy, err := mdnsproxy.NewMDNSProxy(logger.Named("dnsproxy")) if err != nil { return nil, err } @@ -47,8 +48,8 @@ func NewDotLocal(logger *zap.Logger) (*DotLocal, error) { }, nil } -func (d *DotLocal) Start() error { - ctx, cancel := context.WithCancel(context.Background()) +func (d *DotLocal) Start(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) d.ctx = ctx d.cancel = cancel @@ -74,10 +75,10 @@ func (d *DotLocal) Start() error { var t tomb.Tomb t.Go(func() error { - return d.nginx.Start() + return d.nginx.Start(ctx) }) t.Go(func() error { - return d.dnsProxy.Start(d.nginx.Port()) + return d.dnsProxy.Start(ctx) }) err = t.Wait() @@ -178,9 +179,15 @@ func (d *DotLocal) GetMappings() []internal.Mapping { } func (d *DotLocal) CreateMapping(opts internal.MappingOptions) (internal.Mapping, error) { + if !strings.HasSuffix(opts.Host, ".local") { + opts.Host += ".local" + } if opts.PathPrefix == "" { opts.PathPrefix = "/" } + if !strings.HasPrefix(opts.Target, "http://") && !strings.HasPrefix(opts.Target, "https://") { + opts.Target = "http://" + opts.Target + } key := internal.MappingKey{ Host: opts.Host, PathPrefix: opts.PathPrefix, diff --git a/internal/daemon/main.go b/internal/daemon/main.go index 389d87b..dbef113 100644 --- a/internal/daemon/main.go +++ b/internal/daemon/main.go @@ -20,15 +20,15 @@ func Start(logger *zap.Logger) error { return err } - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() var t tomb.Tomb t.Go(func() error { - return dotlocal.Start() + return dotlocal.Start(ctx) }) t.Go(func() error { - return apiServer.Start() + return apiServer.Start(ctx) }) err = t.Wait() if err != nil { diff --git a/internal/daemon/mdnsproxy/controller.go b/internal/daemon/mdnsproxy/controller.go new file mode 100644 index 0000000..8c340d3 --- /dev/null +++ b/internal/daemon/mdnsproxy/controller.go @@ -0,0 +1,82 @@ +package mdnsproxy + +import ( + "context" + + dnssd "github.com/softnetics/dotlocal/dns-sd" + "github.com/softnetics/dotlocal/internal/daemon/dnsproxy" + "go.uber.org/zap" +) + +var nginxImage = "nginx:1.24.0-alpine" + +type MDNSProxy struct { + logger *zap.Logger + dnsService dnssd.DNSService + registeredHosts map[string]dnssd.DNSRecord + + cancelProcess context.CancelFunc +} + +func NewMDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { + return &MDNSProxy{ + logger: logger, + registeredHosts: make(map[string]dnssd.DNSRecord), + }, nil +} + +func (p *MDNSProxy) Start(ctx context.Context) error { + p.logger.Debug("Connecting to dns service") + service, err := dnssd.NewConnection() + if err != nil { + return err + } + p.dnsService = service + p.logger.Info("Ready") + + ctx, cancel := context.WithCancel(ctx) + p.cancelProcess = cancel + go func() { + err := service.Process(ctx) + if err != nil { + p.logger.Error("Failed to process dns service", zap.Error(err)) + } + }() + + return nil +} + +func (p *MDNSProxy) Stop() error { + p.logger.Info("Stopping") + p.cancelProcess() + p.dnsService.Deallocate() + return nil +} + +func (p *MDNSProxy) SetHosts(hostsMap map[string]struct{}) error { + p.logger.Debug("Setting hosts", zap.Any("hosts", hostsMap)) + + for host := range hostsMap { + if _, ok := p.registeredHosts[host]; ok { + continue + } + p.logger.Debug("Adding host", zap.String("host", host)) + record, err := p.dnsService.RegisterProxyAddressRecord(host, "127.0.0.1", 0) + if err != nil { + return err + } + p.registeredHosts[host] = record + } + + for host, record := range p.registeredHosts { + if _, ok := hostsMap[host]; !ok { + p.logger.Debug("Removing host", zap.String("host", host)) + err := p.dnsService.RemoveRecord(record, 0) + if err != nil { + return err + } + delete(p.registeredHosts, host) + } + } + return nil +} diff --git a/internal/daemon/nginx.go b/internal/daemon/nginx.go index 4aab775..06d409e 100644 --- a/internal/daemon/nginx.go +++ b/internal/daemon/nginx.go @@ -2,11 +2,13 @@ package daemon import ( "bufio" + "context" "errors" "fmt" "io" "os" "os/exec" + "path" "strconv" "strings" "sync" @@ -23,7 +25,6 @@ import ( type Nginx struct { logger *zap.Logger configFile string - port int cmd *exec.Cmd mappings []internal.Mapping } @@ -33,22 +34,22 @@ func NewNginx(logger *zap.Logger) (*Nginx, error) { if err != nil { return nil, err } - port, err := util.FindAvailablePort() - if err != nil { - return nil, err - } return &Nginx{ logger: logger, configFile: configFile, - port: port, cmd: nil, mappings: nil, }, nil } -func (n *Nginx) Start() error { +func (n *Nginx) Start(ctx context.Context) error { + err := n.killExistingProcess() + if err != nil { + return err + } + n.writeConfig() - n.logger.Debug("Starting nginx", zap.Int("port", n.port)) + n.logger.Debug("Starting nginx") fmt.Printf("nginx -c %s\n", n.configFile) cmd := exec.Command("nginx", "-c", n.configFile) @@ -76,10 +77,16 @@ func (n *Nginx) Start() error { io.Copy(os.Stdout, stdout) }() + go func() { + <-ctx.Done() + cmd.Process.Signal(syscall.SIGTERM) + }() + err = cmd.Start() if err != nil { return err } + wg.Wait() if !nginxStarted { err := cmd.Wait() @@ -97,6 +104,7 @@ func (n *Nginx) Start() error { func (n *Nginx) SetMappings(mappings []internal.Mapping) error { n.mappings = mappings + n.logger.Debug("Setting mappings", zap.Any("mappings", mappings)) err := n.writeConfig() if err != nil { return err @@ -114,10 +122,6 @@ func (n *Nginx) Stop() error { return nil } -func (n *Nginx) Port() int { - return n.port -} - func (n *Nginx) writeConfig() error { p := parser.NewStringParser(` daemon off; @@ -142,7 +146,7 @@ func (n *Nginx) writeConfig() error { directives := []gonginx.IDirective{ &gonginx.Directive{ Name: "listen", - Parameters: []string{"127.0.0.1:" + strconv.Itoa(n.port)}, + Parameters: []string{"127.0.0.1"}, }, &gonginx.Directive{ Name: "server_name", @@ -210,7 +214,7 @@ func (n *Nginx) writeConfig() error { Directives: []gonginx.IDirective{ &gonginx.Directive{ Name: "listen", - Parameters: []string{"127.0.0.1:" + strconv.Itoa(n.port), "default_server"}, + Parameters: []string{"127.0.0.1", "default_server"}, }, &gonginx.Directive{ Name: "return", @@ -239,3 +243,36 @@ func (n *Nginx) reloadConfig() error { n.logger.Info("Reloaded nginx config") return nil } + +func (n *Nginx) killExistingProcess() error { + pidBytes, err := os.ReadFile(path.Join(util.GetDotlocalPath(), "nginx.pid")) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + pidString := strings.TrimSpace(strings.Split(string(pidBytes), "\n")[0]) + pid, err := strconv.Atoi(pidString) + if err != nil { + return err + } + n.logger.Info("Killing existing process", zap.Int("pid", pid)) + + process, err := os.FindProcess(pid) + if err != nil { + return err + } + _ = process.Signal(syscall.SIGTERM) + + err = os.Remove(util.GetPidPath()) + if err != nil { + return err + } + err = os.Remove(util.GetApiSocketPath()) + if err != nil { + return err + } + + return nil +} diff --git a/internal/daemon/orbdnsproxy/container.go b/internal/daemon/orbdnsproxy/container.go deleted file mode 100644 index a05c2bc..0000000 --- a/internal/daemon/orbdnsproxy/container.go +++ /dev/null @@ -1,80 +0,0 @@ -package orbdnsproxy - -import ( - "context" - "fmt" - - "github.com/dchest/uniuri" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/client" - "go.uber.org/zap" -) - -type Container struct { - logger *zap.Logger - docker *client.Client - configFile string - hostname string - id string -} - -func NewContainer(logger *zap.Logger, docker *client.Client, configFile string, hostname string) (*Container, error) { - return &Container{ - logger: logger, - docker: docker, - configFile: configFile, - hostname: hostname, - }, nil -} - -func (c *Container) CreateAndStart() error { - c.logger.Debug("Creating container", zap.String("hostname", c.hostname)) - - name := fmt.Sprintf("dotlocal-%s-%s", uniuri.NewLen(6), c.hostname) - res, err := c.docker.ContainerCreate(context.Background(), &container.Config{ - Image: "nginx:1.24.0-alpine", - Labels: map[string]string{ - "dev.orbstack.domains": c.hostname, - "managed-dotlocal": "true", - }, - }, &container.HostConfig{ - Mounts: []mount.Mount{ - { - Type: mount.TypeBind, - Source: c.configFile, - Target: "/etc/nginx/conf.d/default.conf", - }, - }, - }, nil, nil, name) - if err != nil { - return err - } - c.id = res.ID - - err = c.docker.ContainerStart(context.Background(), c.id, types.ContainerStartOptions{}) - if err != nil { - return err - } - - c.logger.Info("Started container", zap.String("hostname", c.hostname), zap.String("id", c.id)) - - return nil -} - -func (c *Container) Remove() error { - if c.id == "" { - return nil - } - - c.logger.Debug("Removing container", zap.String("hostname", c.hostname), zap.String("id", c.id)) - err := c.docker.ContainerRemove(context.Background(), c.id, types.ContainerRemoveOptions{ - Force: true, - }) - if err != nil { - return err - } - c.logger.Info("Removed container", zap.String("id", c.id)) - return nil -} diff --git a/internal/daemon/orbdnsproxy/controller.go b/internal/daemon/orbdnsproxy/controller.go deleted file mode 100644 index 5f1e504..0000000 --- a/internal/daemon/orbdnsproxy/controller.go +++ /dev/null @@ -1,209 +0,0 @@ -package orbdnsproxy - -import ( - "context" - "fmt" - "io" - "os" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/client" - "github.com/softnetics/dotlocal/internal/daemon/dnsproxy" - "github.com/softnetics/dotlocal/internal/util" - "github.com/tufanbarisyildirim/gonginx" - "go.uber.org/zap" - "gopkg.in/tomb.v2" -) - -var nginxImage = "nginx:1.24.0-alpine" - -type OrbstackDNSProxy struct { - logger *zap.Logger - docker *client.Client - port int - containers map[string]*Container - nginxConfigFile string -} - -func NewOrbstackDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { - cli, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - return nil, err - } - nginxConfigFile, err := util.CreateTmpFile() - if err != nil { - return nil, err - } - - return &OrbstackDNSProxy{ - logger: logger, - docker: cli, - containers: make(map[string]*Container), - nginxConfigFile: nginxConfigFile, - }, nil -} - -func (p *OrbstackDNSProxy) Start(port int) error { - p.port = port - p.logger.Debug("Ensuring nginx image exists", zap.String("image", nginxImage)) - err := p.writeNginxConfig() - if err != nil { - return err - } - err = ensureImageExists(p.docker, nginxImage) - if err != nil { - return err - } - - p.logger.Debug("Cleaning up existing containers") - containers, err := p.docker.ContainerList(context.Background(), types.ContainerListOptions{ - Filters: filters.NewArgs(filters.Arg("label", "managed-dotlocal")), - }) - if err != nil { - return err - } - for _, container := range containers { - p.logger.Debug("Removing container", zap.String("id", container.ID)) - err := p.docker.ContainerRemove(context.Background(), container.ID, types.ContainerRemoveOptions{ - Force: true, - }) - if err != nil { - return err - } - } - - p.logger.Info("Ready") - - return nil -} - -func (p *OrbstackDNSProxy) Stop() error { - p.logger.Info("Stopping") - var t tomb.Tomb - for _, container := range p.containers { - t.Go(func() error { - return container.Remove() - }) - } - t.Go(func() error { - return os.Remove(p.nginxConfigFile) - }) - return t.Wait() -} - -func (p *OrbstackDNSProxy) SetHosts(hosts map[string]struct{}) error { - p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) - - var t tomb.Tomb - needsWait := false - - for host := range hosts { - _, exists := p.containers[host] - if exists { - continue - } - - container, err := NewContainer(p.logger, p.docker, p.nginxConfigFile, host) - if err != nil { - return err - } - p.containers[host] = container - needsWait = true - t.Go(func() error { - return container.CreateAndStart() - }) - } - - for _host, _container := range p.containers { - host := _host - container := _container - _, exists := hosts[host] - if exists { - continue - } - needsWait = true - t.Go(func() error { - delete(p.containers, host) - return container.Remove() - }) - } - - if !needsWait { - return nil - } - return t.Wait() -} - -func (p *OrbstackDNSProxy) writeNginxConfig() error { - conf := &gonginx.Block{ - Directives: []gonginx.IDirective{ - &gonginx.Directive{ - Name: "server", - Block: &gonginx.Block{ - Directives: []gonginx.IDirective{ - &gonginx.Directive{ - Name: "listen", - Parameters: []string{"80"}, - }, - &gonginx.Directive{ - Name: "location", - Parameters: []string{"/"}, - Block: &gonginx.Block{ - Directives: []gonginx.IDirective{ - &gonginx.Directive{ - Name: "proxy_pass", - Parameters: []string{fmt.Sprintf("http://host.docker.internal:%d", p.port)}, - }, - &gonginx.Directive{ - Name: "proxy_http_version", - Parameters: []string{"1.1"}, - }, - &gonginx.Directive{ - Name: "proxy_set_header", - Parameters: []string{"Upgrade", "$http_upgrade"}, - }, - &gonginx.Directive{ - Name: "proxy_set_header", - Parameters: []string{"Connection", "\"Upgrade\""}, - }, - &gonginx.Directive{ - Name: "proxy_set_header", - Parameters: []string{"Host", "$host"}, - }, - &gonginx.Directive{ - Name: "proxy_set_header", - Parameters: []string{"X-Forwarded-For", "$remote_addr"}, - }, - }, - }, - }, - }, - }, - }, - }, - } - configString := gonginx.DumpBlock(conf, gonginx.IndentedStyle) - p.logger.Debug("Writing nginx config", zap.String("config", configString)) - err := os.WriteFile(p.nginxConfigFile, []byte(configString), 0644) - if err != nil { - return err - } - return nil -} - -func ensureImageExists(cli *client.Client, containerID string) error { - _, _, err := cli.ImageInspectWithRaw(context.Background(), containerID) - if err == nil { - return nil - } - out, err := cli.ImagePull(context.Background(), containerID, types.ImagePullOptions{}) - if err != nil { - return err - } - io.Copy(os.Stdout, out) - - defer out.Close() - - return nil -} diff --git a/internal/util/util.go b/internal/util/util.go index 7dfdd10..55db165 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -19,8 +19,7 @@ var dotlocalPath *string func GetDotlocalPath() string { if dotlocalPath == nil { - home := os.Getenv("HOME") - dir := path.Join(home, ".dotlocal") + dir := "/var/run/dotlocal" dotlocalPath = &dir err := os.MkdirAll(dir, 0755) diff --git a/proto/dot-local.proto b/proto/dot-local.proto index c8c62cb..18d4029 100644 --- a/proto/dot-local.proto +++ b/proto/dot-local.proto @@ -4,7 +4,7 @@ import "google/protobuf/timestamp.proto"; option go_package = "github.com/softnetics/dotlocal/api"; service DotLocal { - rpc CreateMapping (CreateMappingRequest) returns (google.protobuf.Empty) {} + rpc CreateMapping (CreateMappingRequest) returns (Mapping) {} rpc RemoveMapping (MappingKey) returns (google.protobuf.Empty) {} rpc ListMappings (google.protobuf.Empty) returns (ListMappingsResponse) {} } diff --git a/proto/generate b/proto/generate index b66ced1..593eca1 100755 --- a/proto/generate +++ b/proto/generate @@ -6,7 +6,7 @@ protoc proto/*.proto \ --go-grpc_out=./internal/api --go-grpc_opt=paths=source_relative \ --plugin=$HOMEBREW_PREFIX/bin/protoc-gen-swift \ --swift_opt=Visibility=Public \ - --swift_out=DotLocal/Model \ + --swift_out=Shared/Model \ --plugin=$HOMEBREW_PREFIX/bin/protoc-gen-grpc-swift \ --grpc-swift_opt=Visibility=Public \ - --grpc-swift_out=DotLocal/Model + --grpc-swift_out=Shared/Model