From 51f528bcbef7a762c4b3e57214c9927e60d71687 Mon Sep 17 00:00:00 2001 From: Chantawat Yangtrong Date: Thu, 11 Jan 2024 15:45:19 +0700 Subject: [PATCH 01/32] feat: use mdns record and nginx port 80 --- .vscode/settings.json | 5 + c/ClientCommon.c | 76 ++++ c/ClientCommon.h | 41 +++ c/Makefile | 61 ++++ c/c.go | 16 + c/dns-sd.c | 415 ++++++++++++++++++++++ c/dns-sd.h | 1 + go.mod | 17 +- go.sum | 62 ---- internal/daemon/apiserver.go | 4 + internal/daemon/dnsproxy/interface.go | 2 +- internal/daemon/dotlocal.go | 2 +- internal/daemon/nginx.go | 18 +- internal/daemon/orbdnsproxy/container.go | 80 ----- internal/daemon/orbdnsproxy/controller.go | 114 +----- 15 files changed, 643 insertions(+), 271 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 c/ClientCommon.c create mode 100644 c/ClientCommon.h create mode 100644 c/Makefile create mode 100644 c/c.go create mode 100644 c/dns-sd.c create mode 100644 c/dns-sd.h delete mode 100644 internal/daemon/orbdnsproxy/container.go 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/c/ClientCommon.c b/c/ClientCommon.c new file mode 100644 index 0000000..f96319c --- /dev/null +++ b/c/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/c/ClientCommon.h b/c/ClientCommon.h new file mode 100644 index 0000000..afe5b7a --- /dev/null +++ b/c/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/c/Makefile b/c/Makefile new file mode 100644 index 0000000..b8f7f9b --- /dev/null +++ b/c/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/c/c.go b/c/c.go new file mode 100644 index 0000000..23b0166 --- /dev/null +++ b/c/c.go @@ -0,0 +1,16 @@ +package c + +// #cgo CFLAGS: -g -Wall -I./mDNSShared +// #include "dns-sd.h" +import "C" +import ( + "sync" +) + +func StartDNSService(host string) { + mu := sync.Mutex{} + go func() { + mu.Lock() + C.startDNSService(C.CString(host)) + }() +} diff --git a/c/dns-sd.c b/c/dns-sd.c new file mode 100644 index 0000000..f815521 --- /dev/null +++ b/c/dns-sd.c @@ -0,0 +1,415 @@ +#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 + +/** + * 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); +} + +static DNSServiceErrorType RegisterProxyAddressRecord(DNSServiceRef sdref, 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; + static DNSRecordRef record = NULL; + memset(&hostaddr, 0, sizeof(hostaddr)); + getip(ip, &hostaddr); + if (!(flags & kDNSServiceFlagsShared)) + { + flags |= kDNSServiceFlagsUnique; + } + if (hostaddr.ss_family == AF_INET) + return(DNSServiceRegisterRecord(sdref, &record, 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, &record, 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 + +void daemonize() { + pid_t pid, sid; + + // Fork the parent process + pid = fork(); + + // If fork fails, exit + if (pid < 0) { + exit(EXIT_FAILURE); + } + + // If we got a good PID, then we can exit the parent process + if (pid > 0) { + exit(EXIT_SUCCESS); + } + + // Change the file mode mask + umask(0); + + // Create a new SID for the child process + sid = setsid(); + if (sid < 0) { + exit(EXIT_FAILURE); + } + + // Change the current working directory + if ((chdir("/")) < 0) { + exit(EXIT_FAILURE); + } + + // Close standard file descriptors + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); +} + +int main(int argc, char *argv[]) { + char* host = argv[1]; + DNSServiceErrorType err; + printtimestamp(); + printf("...STARTING...\n"); + printf("Registering host to local\n"); + printf("Host: %s\n", host); + printf("Client pa is not null\n"); + err = DNSServiceCreateConnection(&client_pa); + if (err) { fprintf(stderr, "DNSServiceCreateConnection returned %d\n", err); return(err); } + DNSServiceFlags flags = 0; + err = RegisterProxyAddressRecord(client_pa, host, "127.0.0.1", flags); + if (err) { fprintf(stderr, "DNSServiceRegisterRecord returned %d\n", err); return(err); } + HandleEvents(); + + if (client_pa) DNSServiceRefDeallocate(client_pa); + return 0; +} diff --git a/c/dns-sd.h b/c/dns-sd.h new file mode 100644 index 0000000..733e52f --- /dev/null +++ b/c/dns-sd.h @@ -0,0 +1 @@ +int startDNSService(char *host); \ No newline at end of file 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/daemon/apiserver.go b/internal/daemon/apiserver.go index 24a8c58..fae7ded 100644 --- a/internal/daemon/apiserver.go +++ b/internal/daemon/apiserver.go @@ -45,6 +45,10 @@ func (s *APIServer) Start() error { 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)) diff --git a/internal/daemon/dnsproxy/interface.go b/internal/daemon/dnsproxy/interface.go index 4b10f44..1a5052c 100644 --- a/internal/daemon/dnsproxy/interface.go +++ b/internal/daemon/dnsproxy/interface.go @@ -1,7 +1,7 @@ package dnsproxy type DNSProxy interface { - Start(port int) error + Start() error SetHosts(hosts map[string]struct{}) error Stop() error } diff --git a/internal/daemon/dotlocal.go b/internal/daemon/dotlocal.go index ef6ad28..efdad87 100644 --- a/internal/daemon/dotlocal.go +++ b/internal/daemon/dotlocal.go @@ -77,7 +77,7 @@ func (d *DotLocal) Start() error { return d.nginx.Start() }) t.Go(func() error { - return d.dnsProxy.Start(d.nginx.Port()) + return d.dnsProxy.Start() }) err = t.Wait() diff --git a/internal/daemon/nginx.go b/internal/daemon/nginx.go index 4aab775..bd2b311 100644 --- a/internal/daemon/nginx.go +++ b/internal/daemon/nginx.go @@ -7,7 +7,6 @@ import ( "io" "os" "os/exec" - "strconv" "strings" "sync" "syscall" @@ -23,7 +22,6 @@ import ( type Nginx struct { logger *zap.Logger configFile string - port int cmd *exec.Cmd mappings []internal.Mapping } @@ -33,14 +31,9 @@ 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 @@ -48,7 +41,7 @@ func NewNginx(logger *zap.Logger) (*Nginx, error) { func (n *Nginx) Start() error { 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) @@ -97,6 +90,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 +108,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 +132,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 +200,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"}, }, &gonginx.Directive{ Name: "return", 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 index 5f1e504..e101582 100644 --- a/internal/daemon/orbdnsproxy/controller.go +++ b/internal/daemon/orbdnsproxy/controller.go @@ -1,14 +1,9 @@ package orbdnsproxy import ( - "context" - "fmt" - "io" "os" + "os/exec" - "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" @@ -20,17 +15,12 @@ var nginxImage = "nginx:1.24.0-alpine" type OrbstackDNSProxy struct { logger *zap.Logger - docker *client.Client port int - containers map[string]*Container nginxConfigFile string + hostProcesses map[string]*exec.Cmd } 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 @@ -38,54 +28,23 @@ func NewOrbstackDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { return &OrbstackDNSProxy{ logger: logger, - docker: cli, - containers: make(map[string]*Container), nginxConfigFile: nginxConfigFile, }, nil } -func (p *OrbstackDNSProxy) Start(port int) error { - p.port = port +func (p *OrbstackDNSProxy) Start() error { 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) }) @@ -95,44 +54,25 @@ func (p *OrbstackDNSProxy) Stop() error { func (p *OrbstackDNSProxy) SetHosts(hosts map[string]struct{}) error { p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) - var t tomb.Tomb - needsWait := false + newHostProcesses := make(map[string]*exec.Cmd) 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() - }) + p.logger.Debug("Setting host", zap.String("host", host)) + cmd := exec.Command("./c/build/dns-sd", host) + cmd.Start() + newHostProcesses[host] = cmd } - - for _host, _container := range p.containers { - host := _host - container := _container - _, exists := hosts[host] - if exists { - continue + for host, cmd := range p.hostProcesses { + if _, ok := hosts[host]; !ok { + p.logger.Debug("Killing host", zap.String("host", host)) + err := cmd.Process.Kill() + if err != nil { + return err + } } - needsWait = true - t.Go(func() error { - delete(p.containers, host) - return container.Remove() - }) - } - - if !needsWait { - return nil } - return t.Wait() + p.hostProcesses = newHostProcesses + return nil } func (p *OrbstackDNSProxy) writeNginxConfig() error { @@ -151,10 +91,6 @@ func (p *OrbstackDNSProxy) writeNginxConfig() error { 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"}, @@ -191,19 +127,3 @@ func (p *OrbstackDNSProxy) writeNginxConfig() error { } 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 -} From b540a125f8ffefbde707899abd2f32d4166c0145 Mon Sep 17 00:00:00 2001 From: Chantawat Yangtrong Date: Thu, 11 Jan 2024 15:56:36 +0700 Subject: [PATCH 02/32] refactor: remove orbstack --- README.md | 10 ++++------ internal/daemon/dotlocal.go | 6 +++--- .../{orbdnsproxy => mdnsproxy}/controller.go | 16 ++++++++-------- 3 files changed, 15 insertions(+), 17 deletions(-) rename internal/daemon/{orbdnsproxy => mdnsproxy}/controller.go (88%) diff --git a/README.md b/README.md index 33f24d1..2decdad 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ # dotlocal -Local development domains, powered by OrbStack. +Local development domains. ## Usage -1. Start [OrbStack](https://orbstack.dev) +1. Start the dotlocal daemon with root privilege: `dotlocal-daemon` -2. Start the dotlocal daemon: `dotlocal-daemon` +2. Start your app with `dotlocal` wrapper: `dotlocal --host testapp.local pnpm api:dev` -3. Start your app with `dotlocal` wrapper: `dotlocal --host testapp.local pnpm api:dev` - -4. Visit your app at `http://testapp.local`. +3. Visit your app at `http://testapp.local`. diff --git a/internal/daemon/dotlocal.go b/internal/daemon/dotlocal.go index efdad87..67daeea 100644 --- a/internal/daemon/dotlocal.go +++ b/internal/daemon/dotlocal.go @@ -11,7 +11,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 +34,7 @@ func NewDotLocal(logger *zap.Logger) (*DotLocal, error) { return nil, err } - dnsProxy, err := orbdnsproxy.NewOrbstackDNSProxy(logger.Named("orbdnsproxy")) + mdnsProxy, err := mdnsproxy.NewMDNSProxy(logger.Named("dnsproxy")) if err != nil { return nil, err } @@ -42,7 +42,7 @@ func NewDotLocal(logger *zap.Logger) (*DotLocal, error) { return &DotLocal{ logger: logger, nginx: nginx, - dnsProxy: dnsProxy, + dnsProxy: mdnsProxy, mappings: make(map[internal.MappingKey]*internal.MappingState), }, nil } diff --git a/internal/daemon/orbdnsproxy/controller.go b/internal/daemon/mdnsproxy/controller.go similarity index 88% rename from internal/daemon/orbdnsproxy/controller.go rename to internal/daemon/mdnsproxy/controller.go index e101582..f96b443 100644 --- a/internal/daemon/orbdnsproxy/controller.go +++ b/internal/daemon/mdnsproxy/controller.go @@ -1,4 +1,4 @@ -package orbdnsproxy +package mdnsproxy import ( "os" @@ -13,26 +13,26 @@ import ( var nginxImage = "nginx:1.24.0-alpine" -type OrbstackDNSProxy struct { +type MDNSProxy struct { logger *zap.Logger port int nginxConfigFile string hostProcesses map[string]*exec.Cmd } -func NewOrbstackDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { +func NewMDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { nginxConfigFile, err := util.CreateTmpFile() if err != nil { return nil, err } - return &OrbstackDNSProxy{ + return &MDNSProxy{ logger: logger, nginxConfigFile: nginxConfigFile, }, nil } -func (p *OrbstackDNSProxy) Start() error { +func (p *MDNSProxy) Start() error { p.logger.Debug("Ensuring nginx image exists", zap.String("image", nginxImage)) err := p.writeNginxConfig() if err != nil { @@ -42,7 +42,7 @@ func (p *OrbstackDNSProxy) Start() error { return nil } -func (p *OrbstackDNSProxy) Stop() error { +func (p *MDNSProxy) Stop() error { p.logger.Info("Stopping") var t tomb.Tomb t.Go(func() error { @@ -51,7 +51,7 @@ func (p *OrbstackDNSProxy) Stop() error { return t.Wait() } -func (p *OrbstackDNSProxy) SetHosts(hosts map[string]struct{}) error { +func (p *MDNSProxy) SetHosts(hosts map[string]struct{}) error { p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) newHostProcesses := make(map[string]*exec.Cmd) @@ -75,7 +75,7 @@ func (p *OrbstackDNSProxy) SetHosts(hosts map[string]struct{}) error { return nil } -func (p *OrbstackDNSProxy) writeNginxConfig() error { +func (p *MDNSProxy) writeNginxConfig() error { conf := &gonginx.Block{ Directives: []gonginx.IDirective{ &gonginx.Directive{ From 7b8ccace38cd40d113493884dcb02c3c6e02e902 Mon Sep 17 00:00:00 2001 From: Chantawat Yangtrong Date: Thu, 11 Jan 2024 16:40:18 +0700 Subject: [PATCH 03/32] refactor: use single dns-sd process --- c/c.go | 16 ------- c/dns-sd.c | 64 +++++++++---------------- c/dns-sd.h | 1 - internal/daemon/mdnsproxy/controller.go | 36 +++++++------- 4 files changed, 42 insertions(+), 75 deletions(-) delete mode 100644 c/c.go delete mode 100644 c/dns-sd.h diff --git a/c/c.go b/c/c.go deleted file mode 100644 index 23b0166..0000000 --- a/c/c.go +++ /dev/null @@ -1,16 +0,0 @@ -package c - -// #cgo CFLAGS: -g -Wall -I./mDNSShared -// #include "dns-sd.h" -import "C" -import ( - "sync" -) - -func StartDNSService(host string) { - mu := sync.Mutex{} - go func() { - mu.Lock() - C.startDNSService(C.CString(host)) - }() -} diff --git a/c/dns-sd.c b/c/dns-sd.c index f815521..1e113e4 100644 --- a/c/dns-sd.c +++ b/c/dns-sd.c @@ -359,55 +359,37 @@ static void HandleEvents(void) } #endif -void daemonize() { - pid_t pid, sid; - - // Fork the parent process - pid = fork(); - - // If fork fails, exit - if (pid < 0) { - exit(EXIT_FAILURE); - } - - // If we got a good PID, then we can exit the parent process - if (pid > 0) { - exit(EXIT_SUCCESS); - } - - // Change the file mode mask - umask(0); - - // Create a new SID for the child process - sid = setsid(); - if (sid < 0) { - exit(EXIT_FAILURE); - } - - // Change the current working directory - if ((chdir("/")) < 0) { - exit(EXIT_FAILURE); - } - - // Close standard file descriptors - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); +static bool isEndedWithDotLocal(const char *const hostname) { + const char *const dotLocal = ".local"; + const size_t hostnameLen = strlen(hostname); + const size_t dotLocalLen = strlen(dotLocal); + if (hostnameLen < dotLocalLen) return false; + return (strcasecmp(hostname + hostnameLen - dotLocalLen, dotLocal) == 0); } int main(int argc, char *argv[]) { - char* host = argv[1]; + if (argc < 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } DNSServiceErrorType err; + DNSServiceFlags flags = 0; printtimestamp(); printf("...STARTING...\n"); - printf("Registering host to local\n"); - printf("Host: %s\n", host); - printf("Client pa is not null\n"); err = DNSServiceCreateConnection(&client_pa); if (err) { fprintf(stderr, "DNSServiceCreateConnection returned %d\n", err); return(err); } - DNSServiceFlags flags = 0; - err = RegisterProxyAddressRecord(client_pa, host, "127.0.0.1", flags); - if (err) { fprintf(stderr, "DNSServiceRegisterRecord returned %d\n", err); return(err); } + for (int i = 1; i < argc; i++) { + char* host = argv[i]; + if (!isEndedWithDotLocal(host)) { + printf("Adding .local to %s\n", host); + host = malloc(strlen(argv[i]) + 7); + strcpy(host, argv[i]); + strcat(host, ".local"); + } + printf("Registering %s\n", host); + err = RegisterProxyAddressRecord(client_pa, host, "127.0.0.1", flags); + if (err) { fprintf(stderr, "DNSServiceRegisterRecord returned %d\n", err); return(err); } + } HandleEvents(); if (client_pa) DNSServiceRefDeallocate(client_pa); diff --git a/c/dns-sd.h b/c/dns-sd.h deleted file mode 100644 index 733e52f..0000000 --- a/c/dns-sd.h +++ /dev/null @@ -1 +0,0 @@ -int startDNSService(char *host); \ No newline at end of file diff --git a/internal/daemon/mdnsproxy/controller.go b/internal/daemon/mdnsproxy/controller.go index f96b443..8060bfc 100644 --- a/internal/daemon/mdnsproxy/controller.go +++ b/internal/daemon/mdnsproxy/controller.go @@ -17,7 +17,7 @@ type MDNSProxy struct { logger *zap.Logger port int nginxConfigFile string - hostProcesses map[string]*exec.Cmd + command *exec.Cmd } func NewMDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { @@ -51,27 +51,29 @@ func (p *MDNSProxy) Stop() error { return t.Wait() } -func (p *MDNSProxy) SetHosts(hosts map[string]struct{}) error { - p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) +func (p *MDNSProxy) SetHosts(hostsMap map[string]struct{}) error { + p.logger.Debug("Setting hosts", zap.Any("hosts", hostsMap)) - newHostProcesses := make(map[string]*exec.Cmd) + hosts := make([]string, len(hostsMap)) + i := 0 + for host := range hostsMap { + hosts[i] = host + i++ + } - for host := range hosts { - p.logger.Debug("Setting host", zap.String("host", host)) - cmd := exec.Command("./c/build/dns-sd", host) - cmd.Start() - newHostProcesses[host] = cmd + p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) + cmd := exec.Command("./c/build/dns-sd", hosts...) + err := cmd.Start() + if err != nil { + return err } - for host, cmd := range p.hostProcesses { - if _, ok := hosts[host]; !ok { - p.logger.Debug("Killing host", zap.String("host", host)) - err := cmd.Process.Kill() - if err != nil { - return err - } + if p.command != nil { + err := p.command.Process.Kill() + if err != nil { + return err } } - p.hostProcesses = newHostProcesses + p.command = cmd return nil } From 8c1c96319b7a0f763561a1c9567d6b69c5b4001e Mon Sep 17 00:00:00 2001 From: Chantawat Yangtrong Date: Thu, 11 Jan 2024 16:50:25 +0700 Subject: [PATCH 04/32] feat: log stdout --- internal/daemon/mdnsproxy/controller.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/daemon/mdnsproxy/controller.go b/internal/daemon/mdnsproxy/controller.go index 8060bfc..1f5a53e 100644 --- a/internal/daemon/mdnsproxy/controller.go +++ b/internal/daemon/mdnsproxy/controller.go @@ -1,6 +1,7 @@ package mdnsproxy import ( + "bufio" "os" "os/exec" @@ -63,10 +64,23 @@ func (p *MDNSProxy) SetHosts(hostsMap map[string]struct{}) error { p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) cmd := exec.Command("./c/build/dns-sd", hosts...) - err := cmd.Start() + stdout, err := cmd.StdoutPipe() if err != nil { + p.logger.Error("Failed to get stdout pipe", zap.Error(err)) return err } + err = cmd.Start() + if err != nil { + p.logger.Error("Failed to start dns-sd", zap.Error(err)) + return err + } + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + p.logger.Info("dns-sd", zap.String("line", scanner.Text())) + } + p.logger.Info("dns-sd exited") + }() if p.command != nil { err := p.command.Process.Kill() if err != nil { From 2fedb08acfce50c22ab2f0e0aec4c05a8969e562 Mon Sep 17 00:00:00 2001 From: Chantawat Yangtrong Date: Thu, 11 Jan 2024 16:57:17 +0700 Subject: [PATCH 05/32] feat: add dns-sd binary --- cmd/dns-sd/dns-sd | Bin 0 -> 35496 bytes internal/daemon/mdnsproxy/controller.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100755 cmd/dns-sd/dns-sd diff --git a/cmd/dns-sd/dns-sd b/cmd/dns-sd/dns-sd new file mode 100755 index 0000000000000000000000000000000000000000..ab7e08fbfbed00c5afa1e3bc08217333ef49acd8 GIT binary patch literal 35496 zcmeHQeQ;FO6~Avcfz9$2J|vK6vMGiHgzzCk5(3?jgfB7B4GLAtW3&5`tZa6}?gmJs zWycJfjMm9hIx>zeL7nM_S_V7SZk$%)Wb9z4(+;#%>sT9*+C`+JW2enhL;5@Sy-nUe zq}Fyi(?9NI&b#;Ak8{ucopbM9X1Ir6U%hf|GGh`$vVdlQ;u(w`W)74Xn+w_qlI3kx z8*1;Zb#0~E^u!0JS6#=EoGx^ff{m7ib#e z)7M+GR+Hz|a8M%h#^%WZ)JzJOQu1sa2#PnQ?_swQuzE`)fq484hGWQU_5S+1$t zUL{v=t*h6p;nd_XZ~e%BY+~We_FuF(T4;8t4|#jncux zjII2f#;K@tfJn}gws2^P-`B9j+wS**r<4aGJxWge!S-kSitOW$UH$Ccp*!2Kjrcfi zO^{-l%GbxSldf48qMfe~Ii(V;kL1w)GbWn13`A|TW@$|swzo8{3-qaP4@Z=i(siZK z%h!NTNb^A?KL&g#U!e1Z7bU`i>a)Ay-$vl6Q6zzYKtLcM5D*9m1Ox&C0fB%(Kp-Fx z5C{ka1Ofs9fq+0jARrJB2nYlO0s;YnfIvVXAP^7;2m}NI0s(=5KtLcM5D*9m1Ox&C z0fB%(Kp-Fx_|gcBR!e6`Ix_l3tde?qupr*sSr9*NEf|h<%5hs3+n4ysv0S_pS%rVX zJu7CFhi#HGdCAJu5{V@-Sr5xDSAS<=M@m@K6~p-b=y$p^KW<~}l&#R2Wa!_<*b(0M zv2yhn=<7fmc=b_~dk6F5vBBMOn}oS?j^!rwx%l{7wDEmtPgt`~q~?V#J%zt4SC3&l zjiq^aVqUI8b$*=dTD?2oM{AI>_R<=fZR2a`tNvcx*6tflOk>HRY0l)6&~Lz!-pIQ(0*Z(lgr||`76{$)R7(u$WQ6bG3csLyU~V! zkAI4=Igj?|I(O64f}V9}D%7LUVQ>#b9B%Gzp?j?bXrvS%<#}WNgc{z zNr^?zt;5)%9B1;lRXTe4^+z<9Jb*KR2EYWl4 z=dmQmp=-6e9%IfIIFlFVIg`AfgQ*)(XJeY5Y}D=5+bIuod}%#yrY2^xB>5({LwvP5 zo8-(Kmtn;`7p&G38K~#$!uVL{uJ}h*+lg59uJ~ojt-InMBJUf#Z_MxT-Y9unCQB~N zg8V7E{NdH=HHjS=>Db%{JTzRe=8+C--^d_Sd$Et8Z;!2|LM318h40M7yhHGrGr%b^ zktGxG8{1siU=@5{w}B0N33i!56#KNG46i%*-n&G7rL0Hs2<8*MKKL0HI);gH46QBp zsa%Gq95Y5bs{2N?1}heT6VIX_3xr^KrH*ibR-im?}A z+XUvYPlw%5COa+!&i4GM3pn@V$ym?k7jIQlm={9d!d|9YS#-vHVDu;JgcFY-KLovo zcCdc5YxA|kNB-4(g*#zsi!ymJ-2fM7% zbDJd=vP|!O8uokw2Nlr-?s*c|q&1=+uU>-UPIGrdCvas$vEpD%*wlj{k2(& zx)wZW&x0>cx-^sQkTnVZ+YetN%=o%u`nqD7Q5Uswyr_M|x-aq8vD_54G={L^b7nC$ z2K=aRiV}yM<(MP65ZHbQdzDDQ6+HZCc!4CT{Jbh2KiA2w1tYBdbZ5aZ-iFTV#_tH~ zUtk!=#{l~-T}SGhy1U4Bd=2pRGpRL@e_;NUZlo*SL!={}PexzETf$S>7VIxe!JE81 z!BDmv%2N&H8HVy~LwUZTywFfCHI!Ex$`yw4T0?oAp}gKu-fSq>8Okn0`5r_0UPF1e zq1<37HyX|JFBUyuUA6*eIBJcq_`tWbubW6JP}_o;0P&^wopLvI*Pm)=MQ?^e&$k|AXK3@ z)tJK0mMvdZx)gtw83d*JxD-z?kMz6YbzX!7cVXf<6~z<%e@sv zOZ87_(bC(!4hL&eB5to2^ECz?N+=Wz(fCHn!mO6_o}gFZDsBiy9Bxe4>Tl;gLfZUA z;R*+m8Ngn0=T`zL2 zQ+AZz;8XnsJ)fxOIIobhqgi^cJ-g42(z|7^w z`#}$bo&^nnE`lr;CQSw9gO-A}fDqJ7dI)p`bQ1I`=vSckKz{?-txU?*A{8+C#$hFl zZ7!v;MsfQov_wQnZ_uJNXdxtwE%i#I#usjNM?B46Yg5`VN|sxc7Ef!t>}lRhTsY$P z>?KBSbo>12HBD{qkXNhnBAP_2UJSNH^kN8_j0*~-i46tDS+A~x+~W2HE>x0W2boD1;Ox70!amhS(OIiTGNSV58UF&SYQM>uVwrJnRNF$RUQw z2rJ}DOISf;N^Cd+y~B#91*<^0k^1wJUkUI$KuzQjGUKrttn7t0M$sj#>|-y!8}<)Vb>6sO^AQ$ev6 zm;zGI>iobR8vlXL?^&twa4+8fvsD_Ot@9hpG(Jb?7nN)LGU6%NMO5P}b>61)^*TRC z=Mj^S_b<_T1is?@Ql0;<&X?&tST+qhzCe2gWr~9_P12%a5Pz%Q-nv|CUkD!c)Wp+z ze2u(p;@eI9cTD_qCjKQ8PiKKq{%=hDdnW!P6MxmjXKBL5^v^T#i0_p)-%=A_X5y<% z{8kgc%f#czNs}Ko@lg}sW#W5G{81DCyoo<);$JrL6#s;*O>s}i$0^$8-6oKZ0ia{kH#ByWgE4P7eGzY~}p*SkEFOBn2%#{Cj zP6w0Ib~cX6>vlFj!Hg%YIS!DqO&r*1r}zK(z}C)pngn-RORKz|?uFWbSMl!fMVf1Z zku5wXlbi~;v@H}o*j|NbNlG}Jx;Zx@%8_5G-LC{9VSdT@a7WzLo*JTctH!<3fLoKv z9={Jgfq*P~R6#Y8t4U6oy88&*k zJ$WT}+U(Zd#rHh<_>~`YR9(xMz3Fz=x$?s1w_m?)m947jTa`DTeEnqa Date: Thu, 11 Jan 2024 23:03:10 +0700 Subject: [PATCH 06/32] feat: stub helper --- Config/Config.xcconfig | 5 + DotLocal.xcodeproj/project.pbxproj | 161 ++++++++++++++++++- DotLocal/AppConfig.xcconfig | 14 ++ DotLocal/ContentView.swift | 20 +++ DotLocal/LaunchDaemons/helper.plist | 16 ++ DotLocalHelperTool/HelperToolConfig.xcconfig | 14 ++ DotLocalHelperTool/main.swift | 18 +++ Shared/SharedConstants.swift | 12 ++ 8 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 DotLocal/AppConfig.xcconfig create mode 100644 DotLocal/LaunchDaemons/helper.plist create mode 100644 DotLocalHelperTool/HelperToolConfig.xcconfig create mode 100644 DotLocalHelperTool/main.swift create mode 100644 Shared/SharedConstants.swift diff --git a/Config/Config.xcconfig b/Config/Config.xcconfig index 9db76c8..3ad4383 100644 --- a/Config/Config.xcconfig +++ b/Config/Config.xcconfig @@ -9,3 +9,8 @@ // 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) diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index cf00ead..dda01f1 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -22,6 +22,11 @@ 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, ); }; }; + D59D89502B4FFC380009270C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D894F2B4FFC380009270C /* main.swift */; }; + D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */ = {isa = PBXBuildFile; fileRef = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + D59D895C2B4FFF080009270C /* helper.plist in Copy Helper Plist */ = {isa = PBXBuildFile; fileRef = D59D89562B4FFD290009270C /* helper.plist */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + D59D89612B5048C40009270C /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89602B5048C40009270C /* SharedConstants.swift */; }; + D59D89622B5048C40009270C /* SharedConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89602B5048C40009270C /* SharedConstants.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 */; }; @@ -53,6 +58,37 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 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/LaunchDaemons; + dstSubfolderSpec = 1; + files = ( + D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */, + ); + name = "Copy Helper"; + runOnlyForDeploymentPostprocessing = 0; + }; + D59D895B2B4FFEFB0009270C /* Copy Helper Plist */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Contents/Library/LaunchDaemons; + dstSubfolderSpec = 1; + files = ( + D59D895C2B4FFF080009270C /* helper.plist in Copy Helper Plist */, + ); + name = "Copy Helper Plist"; + runOnlyForDeploymentPostprocessing = 0; + }; D5E8DD2A2B47E79700E083E0 /* Copy Daemon */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -109,6 +145,13 @@ 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 /* DotLocalHelperTool */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = DotLocalHelperTool; sourceTree = BUILT_PRODUCTS_DIR; }; + D59D894F2B4FFC380009270C /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + D59D89542B4FFC650009270C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D59D89562B4FFD290009270C /* helper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = helper.plist; 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 = ""; }; 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 = ""; }; @@ -145,6 +188,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D59D894A2B4FFC380009270C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -174,7 +224,9 @@ D582E8FF2B4C5AEE0054343B /* bin */, D5E8DD2D2B47E7B900E083E0 /* dotlocal-daemon */, D5E8DD2B2B47E7AE00E083E0 /* dotlocal */, + D59D895D2B5048420009270C /* Shared */, D529B1AC2B47BF8C00DC288B /* DotLocal */, + D59D894E2B4FFC380009270C /* DotLocalHelperTool */, D529B1C82B47BF8E00DC288B /* DotLocalUITests */, D529B1AB2B47BF8C00DC288B /* Products */, ); @@ -186,6 +238,7 @@ D529B1AA2B47BF8C00DC288B /* DotLocal.app */, D529B1BB2B47BF8E00DC288B /* DotLocalTests.xctest */, D529B1C52B47BF8E00DC288B /* DotLocalUITests.xctest */, + D59D894D2B4FFC380009270C /* DotLocalHelperTool */, ); name = Products; sourceTree = ""; @@ -193,6 +246,8 @@ D529B1AC2B47BF8C00DC288B /* DotLocal */ = { isa = PBXGroup; children = ( + D59D89632B50506D0009270C /* AppConfig.xcconfig */, + D59D89552B4FFCFF0009270C /* LaunchDaemons */, D50377F72B481F69008F9AA8 /* Model */, D529B1AD2B47BF8C00DC288B /* DotLocalApp.swift */, D5E8DD282B47E54800E083E0 /* AppDelegate.swift */, @@ -263,6 +318,32 @@ path = Config; sourceTree = ""; }; + D59D894E2B4FFC380009270C /* DotLocalHelperTool */ = { + isa = PBXGroup; + children = ( + D59D894F2B4FFC380009270C /* main.swift */, + D59D89542B4FFC650009270C /* Info.plist */, + D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */, + ); + path = DotLocalHelperTool; + sourceTree = ""; + }; + D59D89552B4FFCFF0009270C /* LaunchDaemons */ = { + isa = PBXGroup; + children = ( + D59D89562B4FFD290009270C /* helper.plist */, + ); + path = LaunchDaemons; + sourceTree = ""; + }; + D59D895D2B5048420009270C /* Shared */ = { + isa = PBXGroup; + children = ( + D59D89602B5048C40009270C /* SharedConstants.swift */, + ); + path = Shared; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -277,6 +358,8 @@ D529B1D82B47C06400DC288B /* Build golang binaries */, D5E8DD2A2B47E79700E083E0 /* Copy Daemon */, D5E8DD2F2B47E82200E083E0 /* Copy Client */, + D59D89592B4FFDD30009270C /* Copy Helper */, + D59D895B2B4FFEFB0009270C /* Copy Helper Plist */, ); buildRules = ( ); @@ -328,6 +411,23 @@ productReference = D529B1C52B47BF8E00DC288B /* DotLocalUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + D59D894C2B4FFC380009270C /* DotLocalHelperTool */ = { + isa = PBXNativeTarget; + buildConfigurationList = D59D89532B4FFC380009270C /* Build configuration list for PBXNativeTarget "DotLocalHelperTool" */; + buildPhases = ( + D59D89492B4FFC380009270C /* Sources */, + D59D894A2B4FFC380009270C /* Frameworks */, + D59D894B2B4FFC380009270C /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DotLocalHelperTool; + productName = DotLocalHelperTool; + productReference = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; + productType = "com.apple.product-type.tool"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -349,6 +449,9 @@ CreatedOnToolsVersion = 15.1; TestTargetID = D529B1A92B47BF8C00DC288B; }; + D59D894C2B4FFC380009270C = { + CreatedOnToolsVersion = 15.1; + }; }; }; buildConfigurationList = D529B1A52B47BF8C00DC288B /* Build configuration list for PBXProject "DotLocal" */; @@ -370,8 +473,9 @@ projectRoot = ""; targets = ( D529B1A92B47BF8C00DC288B /* DotLocal */, - D529B1BA2B47BF8E00DC288B /* DotLocalTests */, + D59D894C2B4FFC380009270C /* DotLocalHelperTool */, D529B1C42B47BF8E00DC288B /* DotLocalUITests */, + D529B1BA2B47BF8E00DC288B /* DotLocalTests */, ); }; /* End PBXProject section */ @@ -438,6 +542,7 @@ D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */, D50378092B48708C008F9AA8 /* MappingListViewModel.swift in Sources */, D5E8DD332B47E97F00E083E0 /* DaemonManager.swift in Sources */, + D59D89612B5048C40009270C /* SharedConstants.swift in Sources */, D5DEA9B32B4888310029BB00 /* AppMenu.swift in Sources */, D529B1B02B47BF8C00DC288B /* ContentView.swift in Sources */, D503780D2B487250008F9AA8 /* ProtoExtensions.swift in Sources */, @@ -469,6 +574,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D59D89492B4FFC380009270C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D59D89622B5048C40009270C /* SharedConstants.swift in Sources */, + D59D89502B4FFC380009270C /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -487,7 +601,6 @@ /* 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 +664,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 +719,7 @@ }; D529B1D02B47BF8E00DC288B /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D59D89632B50506D0009270C /* AppConfig.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -627,7 +740,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 +748,7 @@ }; D529B1D12B47BF8E00DC288B /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D59D89632B50506D0009270C /* AppConfig.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -656,7 +769,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 +847,36 @@ }; name = Release; }; + D59D89512B4FFC380009270C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 2HCNNF2TB5; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = "$(SRCROOT)/DotLocalHelperTool/Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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; + INFOPLIST_FILE = "$(SRCROOT)/DotLocalHelperTool/Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -774,6 +916,15 @@ 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 */ diff --git a/DotLocal/AppConfig.xcconfig b/DotLocal/AppConfig.xcconfig new file mode 100644 index 0000000..8269549 --- /dev/null +++ b/DotLocal/AppConfig.xcconfig @@ -0,0 +1,14 @@ +// +// 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" + +PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER) +SWIFT_ACTIVE_COMPILATION_CONDITIONS = APP diff --git a/DotLocal/ContentView.swift b/DotLocal/ContentView.swift index 2b5b4b6..65c6928 100644 --- a/DotLocal/ContentView.swift +++ b/DotLocal/ContentView.swift @@ -6,12 +6,32 @@ // import SwiftUI +import ServiceManagement struct ContentView: View { @StateObject var daemonManager = DaemonManager.shared var body: some View { VStack { + Button(action: { + let service = SMAppService.daemon(plistName: "helper.plist") + do { + print("status: \(service.status.rawValue)") + if service.status == .enabled { + print("will unregister") + try service.unregister() + print("did unregister") + } else { + print("will register") + try service.register() + print("did register") + } + } catch { + print("error: \(error)") + } + }, label: { + Text("Test") + }) switch daemonManager.state { case .stopped: Text("DotLocal is not running") diff --git a/DotLocal/LaunchDaemons/helper.plist b/DotLocal/LaunchDaemons/helper.plist new file mode 100644 index 0000000..3f075e7 --- /dev/null +++ b/DotLocal/LaunchDaemons/helper.plist @@ -0,0 +1,16 @@ + + + + + Label + dev.suphon.DotLocal.helper + BundleProgram + Contents/Library/LaunchDaemons/DotLocalHelperTool + RunAtLoad + + StandardOutPath + /tmp/dotlocal.out + StandardErrPath + /tmp/dotlocal.err + + diff --git a/DotLocalHelperTool/HelperToolConfig.xcconfig b/DotLocalHelperTool/HelperToolConfig.xcconfig new file mode 100644 index 0000000..2f83b97 --- /dev/null +++ b/DotLocalHelperTool/HelperToolConfig.xcconfig @@ -0,0 +1,14 @@ +// +// 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" + +PRODUCT_BUNDLE_IDENTIFIER = $(HELPER_TOOL_BUNDLE_IDENTIFIER) +SWIFT_ACTIVE_COMPILATION_CONDITIONS = HELPER_TOOL diff --git a/DotLocalHelperTool/main.swift b/DotLocalHelperTool/main.swift new file mode 100644 index 0000000..71ae302 --- /dev/null +++ b/DotLocalHelperTool/main.swift @@ -0,0 +1,18 @@ +// +// main.swift +// DotLocalHelperTool +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +import Foundation +import os + +func main() { + while true { + NSLog("Hello, World! i am \"\(ProcessInfo.processInfo.userName)\"") + sleep(1) + } +} + +main() diff --git a/Shared/SharedConstants.swift b/Shared/SharedConstants.swift new file mode 100644 index 0000000..51abef7 --- /dev/null +++ b/Shared/SharedConstants.swift @@ -0,0 +1,12 @@ +// +// SharedConstants.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 11/1/2567 BE. +// + +import Foundation + +struct SharedConstants { + +} From 30fe51551be9210321cc4c94ef9118adcfe38ada Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:47:23 +0700 Subject: [PATCH 07/32] feat: helper install flow --- Config/Config.xcconfig | 1 + DotLocal.xcodeproj/project.pbxproj | 138 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 18 +++ DotLocal/AppDelegate.swift | 2 + DotLocal/ContentView.swift | 79 ++++++---- DotLocal/HelperManager.swift | 44 ++++++ DotLocal/LaunchDaemons/helper.plist | 5 + DotLocal/SettingsView.swift | 1 + DotLocalHelperTool/HelperToolConfig.xcconfig | 12 ++ DotLocalHelperTool/Info.plist | 5 + DotLocalHelperTool/launchd.plist | 10 ++ DotLocalHelperTool/main.swift | 26 +++- Shared/CodeInfo.swift | 127 ++++++++++++++++ Shared/HelperToolInfoPropertyList.swift | 42 ++++++ Shared/HelperToolLaunchdPropertyList.swift | 39 +++++ Shared/MyError.swift | 12 ++ Shared/SharedConstants.swift | 3 +- 17 files changed, 522 insertions(+), 42 deletions(-) create mode 100644 DotLocal/HelperManager.swift create mode 100644 DotLocalHelperTool/Info.plist create mode 100644 DotLocalHelperTool/launchd.plist create mode 100644 Shared/CodeInfo.swift create mode 100644 Shared/HelperToolInfoPropertyList.swift create mode 100644 Shared/HelperToolLaunchdPropertyList.swift create mode 100644 Shared/MyError.swift diff --git a/Config/Config.xcconfig b/Config/Config.xcconfig index 3ad4383..7b78d89 100644 --- a/Config/Config.xcconfig +++ b/Config/Config.xcconfig @@ -14,3 +14,4 @@ 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 dda01f1..bdb134e 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -21,12 +21,27 @@ 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 */; }; + D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5793C482B50E8D400979DC3 /* HelperManager.swift */; }; D582E9072B4C5BE20054343B /* nginx in Copy Binaries */ = {isa = PBXBuildFile; fileRef = D582E9052B4C5BC00054343B /* nginx */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D59D89502B4FFC380009270C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D894F2B4FFC380009270C /* main.swift */; }; D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */ = {isa = PBXBuildFile; fileRef = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D59D895C2B4FFF080009270C /* helper.plist in Copy Helper Plist */ = {isa = PBXBuildFile; fileRef = D59D89562B4FFD290009270C /* helper.plist */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 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 */; }; + D59D89702B5055C00009270C /* EmbeddedPropertyList in Frameworks */ = {isa = PBXBuildFile; productRef = D59D896F2B5055C00009270C /* 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 */; }; + D59D89802B505EFE0009270C /* ManageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897F2B505EFE0009270C /* ManageClient.swift */; }; + D59D89812B505EFE0009270C /* ManageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897F2B505EFE0009270C /* ManageClient.swift */; }; + D59D89832B5065CD0009270C /* MyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89822B5065CD0009270C /* MyError.swift */; }; + D59D89842B5065CD0009270C /* MyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89822B5065CD0009270C /* MyError.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 */; }; @@ -142,16 +157,22 @@ 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 = ""; }; + D5793C482B50E8D400979DC3 /* HelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperManager.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 /* DotLocalHelperTool */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = DotLocalHelperTool; sourceTree = BUILT_PRODUCTS_DIR; }; D59D894F2B4FFC380009270C /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - D59D89542B4FFC650009270C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D59D89562B4FFD290009270C /* helper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = helper.plist; 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 = ""; }; + D59D89822B5065CD0009270C /* MyError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyError.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 = ""; }; @@ -170,7 +191,9 @@ files = ( D5DEA9BC2B4995BC0029BB00 /* Defaults in Frameworks */, D5DEA9B62B49936B0029BB00 /* LaunchAtLogin in Frameworks */, + D59D89792B505C430009270C /* SecureXPC in Frameworks */, D50377F62B481B59008F9AA8 /* GRPC in Frameworks */, + D59D896E2B5055BB0009270C /* EmbeddedPropertyList in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -192,6 +215,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D59D89702B5055C00009270C /* EmbeddedPropertyList in Frameworks */, + D59D897B2B505C4B0009270C /* SecureXPC in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -229,6 +254,7 @@ D59D894E2B4FFC380009270C /* DotLocalHelperTool */, D529B1C82B47BF8E00DC288B /* DotLocalUITests */, D529B1AB2B47BF8C00DC288B /* Products */, + D59D896C2B5055BB0009270C /* Frameworks */, ); sourceTree = ""; }; @@ -259,6 +285,7 @@ D529B1B12B47BF8E00DC288B /* Assets.xcassets */, D529B1B62B47BF8E00DC288B /* DotLocal.entitlements */, D529B1B32B47BF8E00DC288B /* Preview Content */, + D5793C482B50E8D400979DC3 /* HelperManager.swift */, D5E8DD322B47E97F00E083E0 /* DaemonManager.swift */, D5DEA9C32B49C0200029BB00 /* ClientManager.swift */, D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */, @@ -321,9 +348,10 @@ D59D894E2B4FFC380009270C /* DotLocalHelperTool */ = { isa = PBXGroup; children = ( - D59D894F2B4FFC380009270C /* main.swift */, - D59D89542B4FFC650009270C /* Info.plist */, D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */, + D59D89712B50572A0009270C /* Info.plist */, + D59D894F2B4FFC380009270C /* main.swift */, + D59D897F2B505EFE0009270C /* ManageClient.swift */, ); path = DotLocalHelperTool; sourceTree = ""; @@ -340,10 +368,21 @@ isa = PBXGroup; children = ( D59D89602B5048C40009270C /* SharedConstants.swift */, + D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */, + D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */, + D59D897C2B505E4B0009270C /* CodeInfo.swift */, + D59D89822B5065CD0009270C /* MyError.swift */, ); path = Shared; sourceTree = ""; }; + D59D896C2B5055BB0009270C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -370,6 +409,8 @@ D50377F52B481B59008F9AA8 /* GRPC */, D5DEA9B52B49936B0029BB00 /* LaunchAtLogin */, D5DEA9BB2B4995BC0029BB00 /* Defaults */, + D59D896D2B5055BB0009270C /* EmbeddedPropertyList */, + D59D89782B505C430009270C /* SecureXPC */, ); productName = DotLocal; productReference = D529B1AA2B47BF8C00DC288B /* DotLocal.app */; @@ -415,15 +456,21 @@ isa = PBXNativeTarget; buildConfigurationList = D59D89532B4FFC380009270C /* Build configuration list for PBXNativeTarget "DotLocalHelperTool" */; buildPhases = ( + D59D89722B505A550009270C /* ShellScript */, D59D89492B4FFC380009270C /* Sources */, D59D894A2B4FFC380009270C /* Frameworks */, D59D894B2B4FFC380009270C /* CopyFiles */, + D59D89732B505B260009270C /* ShellScript */, ); buildRules = ( ); dependencies = ( ); name = DotLocalHelperTool; + packageProductDependencies = ( + D59D896F2B5055C00009270C /* EmbeddedPropertyList */, + D59D897A2B505C4B0009270C /* SecureXPC */, + ); productName = DotLocalHelperTool; productReference = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; productType = "com.apple.product-type.tool"; @@ -467,6 +514,8 @@ D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */, D5DEA9B42B49936B0029BB00 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, D5DEA9BA2B4995BC0029BB00 /* XCRemoteSwiftPackageReference "Defaults" */, + D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */, + D59D89772B505C430009270C /* XCRemoteSwiftPackageReference "SecureXPC" */, ); productRefGroup = D529B1AB2B47BF8C00DC288B /* Products */; projectDirPath = ""; @@ -531,6 +580,40 @@ 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"; }; + D59D89722B505A550009270C /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "LAUNCHDPLIST_FILE=$PROJECT_DIR/$LAUNCHDPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleIdentifier\" $INFOPLIST_FILE || true\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleVersion\" $INFOPLIST_FILE || true\nrm $LAUNCHDPLIST_FILE\n\n/usr/libexec/PlistBuddy -c \"Add :CFBundleIdentifier string $HELPER_TOOL_BUNDLE_IDENTIFIER\" $INFOPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Add :CFBundleVersion string $HELPER_VERSION\" $INFOPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Add :MachServices array\" $LAUNCHDPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Add :MachServices: string $HELPER_TOOL_BUNDLE_IDENTIFIER\" $LAUNCHDPLIST_FILE\n"; + }; + D59D89732B505B260009270C /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "LAUNCHDPLIST_FILE=$PROJECT_DIR/$LAUNCHDPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleIdentifier\" $INFOPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleVersion\" $INFOPLIST_FILE\n#rm $LAUNCHDPLIST_FILE\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -538,14 +621,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D59D89802B505EFE0009270C /* ManageClient.swift in Sources */, + D59D897D2B505E4B0009270C /* CodeInfo.swift in Sources */, D5E8DD292B47E54800E083E0 /* AppDelegate.swift in Sources */, D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */, + D59D89862B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, + D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */, D50378092B48708C008F9AA8 /* MappingListViewModel.swift in Sources */, D5E8DD332B47E97F00E083E0 /* DaemonManager.swift in Sources */, D59D89612B5048C40009270C /* SharedConstants.swift in Sources */, D5DEA9B32B4888310029BB00 /* AppMenu.swift in Sources */, + D59D89832B5065CD0009270C /* MyError.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 */, @@ -578,7 +667,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D59D89842B5065CD0009270C /* MyError.swift in Sources */, + D59D897E2B505E4B0009270C /* CodeInfo.swift in Sources */, D59D89622B5048C40009270C /* SharedConstants.swift in Sources */, + D59D89812B505EFE0009270C /* ManageClient.swift in Sources */, + D59D89872B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, + D59D896A2B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */, D59D89502B4FFC380009270C /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -854,6 +948,7 @@ 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)"; @@ -869,6 +964,7 @@ 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)"; @@ -936,6 +1032,22 @@ minimumVersion = 1.21.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"; @@ -960,6 +1072,26 @@ package = D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */; productName = GRPC; }; + D59D896D2B5055BB0009270C /* EmbeddedPropertyList */ = { + isa = XCSwiftPackageProductDependency; + package = D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; + productName = EmbeddedPropertyList; + }; + D59D896F2B5055C00009270C /* 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..42471cb 100644 --- a/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,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 +36,15 @@ "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc" } }, + { + "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/AppDelegate.swift b/DotLocal/AppDelegate.swift index 3ae59ba..647f7e2 100644 --- a/DotLocal/AppDelegate.swift +++ b/DotLocal/AppDelegate.swift @@ -8,11 +8,13 @@ import Foundation import AppKit import Defaults +import SecureXPC class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { DaemonManager.shared.start() ClientManager.shared.checkInstalled() + _ = HelperManager.shared } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/DotLocal/ContentView.swift b/DotLocal/ContentView.swift index 65c6928..320c8d9 100644 --- a/DotLocal/ContentView.swift +++ b/DotLocal/ContentView.swift @@ -10,40 +10,27 @@ import ServiceManagement struct ContentView: View { @StateObject var daemonManager = DaemonManager.shared + @StateObject var helperManager = HelperManager.shared var body: some View { - VStack { - Button(action: { - let service = SMAppService.daemon(plistName: "helper.plist") - do { - print("status: \(service.status.rawValue)") - if service.status == .enabled { - print("will unregister") - try service.unregister() - print("did unregister") - } else { - print("will register") - try service.register() - print("did register") - } - } catch { - print("error: \(error)") + switch helperManager.status { + case .requiresApproval: + RequiresApprovalView() + case .enabled: + VStack { + switch daemonManager.state { + case .stopped: + Text("DotLocal is not running") + case .starting: + ProgressView() + case .started: + MappingList() } - }, label: { - Text("Test") - }) - switch daemonManager.state { - case .stopped: - Text("DotLocal is not running") - case .starting: - ProgressView() - case .started: - MappingList() + }.toolbar() { + StartStopButton(state: daemonManager.state, onStart: { daemonManager.start() }, onStop: { daemonManager.stop() }) } - } - .frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/, maxHeight: .infinity) - .toolbar() { - StartStopButton(state: daemonManager.state, onStart: { daemonManager.start() }, onStop: { daemonManager.stop() }) + default: + Text("Unexpected state: \(helperManager.status.rawValue)") } } } @@ -69,6 +56,36 @@ struct StartStopButton: View { } } +struct RequiresApprovalView: View { + @State var openedSettings = false + + var body: some View { + VStack(spacing: 8) { + Text("Helper Not Enabled").font(.title).fontWeight(.bold) + Text("Please enable DotLocal in the \"Allow in the Background\" section") + Button(action: { + print("previous status: \(HelperManager.shared.status)") + HelperManager.shared.checkStatus() + print("status: \(HelperManager.shared.status)") + if HelperManager.shared.status == .requiresApproval { + openedSettings = true + SMAppService.openSystemSettingsLoginItems() + } + }, label: { + if openedSettings { + Text("Continue") + } else { + Text("Open System Settings") + } + }) + }.foregroundStyle(.secondary) + } +} + +//#Preview { +// ContentView() +//} + #Preview { - ContentView() + RequiresApprovalView() } diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift new file mode 100644 index 0000000..8c44f5d --- /dev/null +++ b/DotLocal/HelperManager.swift @@ -0,0 +1,44 @@ +// +// 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 service = SMAppService.daemon(plistName: "helper.plist") + @Published var status: SMAppService.Status + + let xpcClient = XPCClient.forMachService(named: "dev.suphon.DotLocal.helper") + + private init() { + status = service.status + Task { + print("sending exit to current helper") + do { + try await xpcClient.send(to: SharedConstants.exitRoute) + } catch { + print("error sending exit: \(error)") + } + do { + print("registering service") + try service.register() + print("registered service") + } catch { + print("error registering service: \(error)") + } + await checkStatus() + } + } + + @MainActor + func checkStatus() { + status = service.status + } +} diff --git a/DotLocal/LaunchDaemons/helper.plist b/DotLocal/LaunchDaemons/helper.plist index 3f075e7..d0ce49f 100644 --- a/DotLocal/LaunchDaemons/helper.plist +++ b/DotLocal/LaunchDaemons/helper.plist @@ -6,6 +6,11 @@ dev.suphon.DotLocal.helper BundleProgram Contents/Library/LaunchDaemons/DotLocalHelperTool + MachServices + + dev.suphon.DotLocal.helper + + RunAtLoad StandardOutPath 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/DotLocalHelperTool/HelperToolConfig.xcconfig b/DotLocalHelperTool/HelperToolConfig.xcconfig index 2f83b97..62396c0 100644 --- a/DotLocalHelperTool/HelperToolConfig.xcconfig +++ b/DotLocalHelperTool/HelperToolConfig.xcconfig @@ -10,5 +10,17 @@ #include "Config/Config.xcconfig" +// The directory containing the source code and property lists for the helper tool. +TARGET_DIRECTORY = DotLocalHelperTool + +// 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) + PRODUCT_BUNDLE_IDENTIFIER = $(HELPER_TOOL_BUNDLE_IDENTIFIER) SWIFT_ACTIVE_COMPILATION_CONDITIONS = HELPER_TOOL diff --git a/DotLocalHelperTool/Info.plist b/DotLocalHelperTool/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/DotLocalHelperTool/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/DotLocalHelperTool/launchd.plist b/DotLocalHelperTool/launchd.plist new file mode 100644 index 0000000..b702d49 --- /dev/null +++ b/DotLocalHelperTool/launchd.plist @@ -0,0 +1,10 @@ + + + + + MachServices + + dev.suphon.DotLocal.helper + + + diff --git a/DotLocalHelperTool/main.swift b/DotLocalHelperTool/main.swift index 71ae302..fb58c54 100644 --- a/DotLocalHelperTool/main.swift +++ b/DotLocalHelperTool/main.swift @@ -6,13 +6,25 @@ // import Foundation -import os +import SecureXPC -func main() { - while true { - NSLog("Hello, World! i am \"\(ProcessInfo.processInfo.userName)\"") - sleep(1) +NSLog("starting helper tool. PID \(getpid()). PPID \(getppid()).") +NSLog("version: \(try HelperToolInfoPropertyList.main.version.rawValue)") + +if getppid() == 1 { + let server = try XPCServer.forMachService() + server.registerRoute(SharedConstants.exitRoute, handler: { + NSLog("exiting") + exit(0) + }) + 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)") + } } + server.startAndBlock() +} else { + print("not supported") } - -main() diff --git a/Shared/CodeInfo.swift b/Shared/CodeInfo.swift new file mode 100644 index 0000000..7a1180e --- /dev/null +++ b/Shared/CodeInfo.swift @@ -0,0 +1,127 @@ +// +// CodeInfo.swift +// SwiftAuthorizationSample +// +// Created by Josh Kaplan on 2021-10-24 +// + +import Foundation + +/// Convenience wrappers around Security framework functionality. +enum CodeInfo { + /// Errors that may occur when trying to determine information about this running helper tool or another on disk executable. + enum CodeInfoError: Error { + /// Unable to determine the location of the executable. + case codeLocationNotRetrievable(OSStatus) + /// Unable to retrieve the on disk code representation for a specified file URL. + case externalStaticCodeNotRetrievable(OSStatus) + /// Unable to retrieve the on disk code representation for this code. + case helperToolStaticCodeNotRetrievable(OSStatus) + /// Unable to retrieve the leaf certificate for a code instance. + case leafCertificateNotRetrievable + /// Unable to retrieve the signing key information for the provided on disk code representation. + case signingKeyDataNotRetrievable + } + + /// Returns the on disk location this code is running from. + /// + /// - Throws: If unable to determine location. + /// - Returns: On disk location of this helper tool. + static func currentCodeLocation() throws -> 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/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/MyError.swift b/Shared/MyError.swift new file mode 100644 index 0000000..a5a819f --- /dev/null +++ b/Shared/MyError.swift @@ -0,0 +1,12 @@ +// +// MyError.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation + +enum MyError: Error { + case runtimeError(String) +} diff --git a/Shared/SharedConstants.swift b/Shared/SharedConstants.swift index 51abef7..768a13b 100644 --- a/Shared/SharedConstants.swift +++ b/Shared/SharedConstants.swift @@ -6,7 +6,8 @@ // import Foundation +import SecureXPC struct SharedConstants { - + static let exitRoute = XPCRoute.named("exit") } From b26bd77d733dfd0838993ea0d669798be1076d00 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:56:19 +0700 Subject: [PATCH 08/32] feat: move cli management to helper --- DotLocal.xcodeproj/project.pbxproj | 4 -- DotLocal/ClientManager.swift | 18 ++++-- DotLocal/Sudo.swift | 86 --------------------------- DotLocalHelperTool/ManageClient.swift | 46 ++++++++++++++ DotLocalHelperTool/main.swift | 2 + Shared/SharedConstants.swift | 2 + 6 files changed, 63 insertions(+), 95 deletions(-) delete mode 100644 DotLocal/Sudo.swift create mode 100644 DotLocalHelperTool/ManageClient.swift diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index bdb134e..aba3627 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -47,7 +47,6 @@ 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, ); }; }; @@ -176,7 +175,6 @@ 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 = ""; }; @@ -290,7 +288,6 @@ D5DEA9C32B49C0200029BB00 /* ClientManager.swift */, D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */, D5DEA9BF2B499C2F0029BB00 /* Defaults.swift */, - D5DEA9C12B49A6B60029BB00 /* Sudo.swift */, ); path = DotLocal; sourceTree = ""; @@ -640,7 +637,6 @@ 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 */, D5DEA9C42B49C0200029BB00 /* ClientManager.swift in Sources */, ); diff --git a/DotLocal/ClientManager.swift b/DotLocal/ClientManager.swift index fb5f957..3579284 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.send(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/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/DotLocalHelperTool/ManageClient.swift b/DotLocalHelperTool/ManageClient.swift new file mode 100644 index 0000000..670d43c --- /dev/null +++ b/DotLocalHelperTool/ManageClient.swift @@ -0,0 +1,46 @@ +// +// ManageCLI.swift +// DotLocal +// +// Created by Suphon Thanakornpakapong on 12/1/2567 BE. +// + +import Foundation + +private func parentAppURL() throws -> URL { + let components = Bundle.main.bundleURL.pathComponents + guard let contentsIndex = components.lastIndex(of: "Contents"), + components[components.index(before: contentsIndex)].hasSuffix(".app") else { + throw MyError.runtimeError(""" + Parent bundle could not be found. + Path:\(Bundle.main.bundleURL) + """) + } + + return URL(fileURLWithPath: "/" + components[1.. URL { + let appURL = try! parentAppURL() + return appURL.appendingPathComponent("Contents/Resources/bin/dotlocal") +} + +private let installLocation = URL.init(filePath: "/usr/local/bin/dotlocal") + +enum ManageClient { + static func install() throws { + let source = clientLocation() + NSLog("installing client") + NSLog("symlink \(source) to \(installLocation)") + try FileManager.default.createDirectory(at: installLocation.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink(at: installLocation, withDestinationURL: source) + 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/main.swift b/DotLocalHelperTool/main.swift index fb58c54..f102bee 100644 --- a/DotLocalHelperTool/main.swift +++ b/DotLocalHelperTool/main.swift @@ -13,6 +13,8 @@ NSLog("version: \(try HelperToolInfoPropertyList.main.version.rawValue)") if getppid() == 1 { let server = try XPCServer.forMachService() + server.registerRoute(SharedConstants.installClientRoute, handler: ManageClient.install) + server.registerRoute(SharedConstants.uninstallClientRoute, handler: ManageClient.uninstall) server.registerRoute(SharedConstants.exitRoute, handler: { NSLog("exiting") exit(0) diff --git a/Shared/SharedConstants.swift b/Shared/SharedConstants.swift index 768a13b..86fe580 100644 --- a/Shared/SharedConstants.swift +++ b/Shared/SharedConstants.swift @@ -9,5 +9,7 @@ import Foundation import SecureXPC struct SharedConstants { + static let installClientRoute = XPCRoute.named("installClient") + static let uninstallClientRoute = XPCRoute.named("uninstallClient") static let exitRoute = XPCRoute.named("exit") } From e65364be4b6460f6042ca5ccc29a5f28a1fe91d0 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 15:42:36 +0700 Subject: [PATCH 09/32] feat: move daemon to helper service --- DotLocal.xcodeproj/project.pbxproj | 62 ++++++-- DotLocal/AppDelegate.swift | 12 +- DotLocal/AppMenu.swift | 12 +- DotLocal/ContentView.swift | 14 +- DotLocal/DaemonManager.swift | 123 +++++----------- DotLocal/HelperManager.swift | 17 ++- DotLocal/MappingList.swift | 15 +- DotLocal/MappingListViewModel.swift | 35 ----- DotLocal/Model/ProtoExtensions.swift | 24 +++ DotLocalHelperTool/DaemonManager.swift | 171 ++++++++++++++++++++++ DotLocalHelperTool/ManageClient.swift | 13 -- DotLocalHelperTool/main.swift | 21 ++- Shared/DaemonState.swift | 15 ++ Shared/SharedConstants.swift | 6 + internal/daemon/apiserver.go | 4 + internal/daemon/orbdnsproxy/controller.go | 4 + internal/util/util.go | 13 +- proto/generate | 4 +- 18 files changed, 379 insertions(+), 186 deletions(-) delete mode 100644 DotLocal/MappingListViewModel.swift create mode 100644 DotLocalHelperTool/DaemonManager.swift create mode 100644 Shared/DaemonState.swift diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index aba3627..a841d06 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 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 */; }; @@ -21,7 +20,16 @@ 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 */; }; + D56116AA2B510CFD00FEB087 /* DaemonManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116A92B510CFD00FEB087 /* DaemonManager.swift */; }; + D56116B22B51109900FEB087 /* dot-local.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378012B482578008F9AA8 /* dot-local.grpc.swift */; }; + D56116B32B51109900FEB087 /* dot-local.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378022B482578008F9AA8 /* dot-local.pb.swift */; }; + D56116B42B51109900FEB087 /* preferences.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378032B482578008F9AA8 /* preferences.pb.swift */; }; + D56116B52B51109900FEB087 /* ProtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503780C2B487250008F9AA8 /* ProtoExtensions.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 */; }; D582E9072B4C5BE20054343B /* nginx in Copy Binaries */ = {isa = PBXBuildFile; fileRef = D582E9052B4C5BC00054343B /* nginx */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D59D89502B4FFC380009270C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D894F2B4FFC380009270C /* main.swift */; }; D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */ = {isa = PBXBuildFile; fileRef = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -31,12 +39,10 @@ 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 */; }; - D59D89702B5055C00009270C /* EmbeddedPropertyList in Frameworks */ = {isa = PBXBuildFile; productRef = D59D896F2B5055C00009270C /* 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 */; }; - D59D89802B505EFE0009270C /* ManageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897F2B505EFE0009270C /* ManageClient.swift */; }; D59D89812B505EFE0009270C /* ManageClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D897F2B505EFE0009270C /* ManageClient.swift */; }; D59D89832B5065CD0009270C /* MyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89822B5065CD0009270C /* MyError.swift */; }; D59D89842B5065CD0009270C /* MyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89822B5065CD0009270C /* MyError.swift */; }; @@ -69,6 +75,13 @@ remoteGlobalIDString = D529B1A92B47BF8C00DC288B; remoteInfo = DotLocal; }; + D56116B02B51102F00FEB087 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D529B1A22B47BF8C00DC288B /* Project object */; + proxyType = 1; + remoteGlobalIDString = D59D894C2B4FFC380009270C; + remoteInfo = DotLocalHelperTool; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -142,7 +155,6 @@ 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; }; @@ -156,7 +168,9 @@ 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 = ""; }; D5793C482B50E8D400979DC3 /* HelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperManager.swift; sourceTree = ""; }; + D5793C5F2B5103E300979DC3 /* DaemonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaemonState.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 = ""; }; @@ -213,8 +227,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D59D89702B5055C00009270C /* EmbeddedPropertyList in Frameworks */, + D5793C5C2B5103CC00979DC3 /* GRPC in Frameworks */, D59D897B2B505C4B0009270C /* SecureXPC in Frameworks */, + D5793C5E2B5103CF00979DC3 /* EmbeddedPropertyList in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -227,7 +242,8 @@ D50378072B482588008F9AA8 /* proto */, D503780C2B487250008F9AA8 /* ProtoExtensions.swift */, ); - path = Model; + name = Model; + path = ../DotLocal/Model; sourceTree = ""; }; D50378072B482588008F9AA8 /* proto */ = { @@ -272,12 +288,10 @@ children = ( D59D89632B50506D0009270C /* AppConfig.xcconfig */, D59D89552B4FFCFF0009270C /* LaunchDaemons */, - D50377F72B481F69008F9AA8 /* Model */, D529B1AD2B47BF8C00DC288B /* DotLocalApp.swift */, D5E8DD282B47E54800E083E0 /* AppDelegate.swift */, D5DEA9B22B4888310029BB00 /* AppMenu.swift */, D529B1AF2B47BF8C00DC288B /* ContentView.swift */, - D50378082B48708C008F9AA8 /* MappingListViewModel.swift */, D503780A2B48718D008F9AA8 /* MappingList.swift */, D529B1BE2B47BF8E00DC288B /* DotLocalTests */, D529B1B12B47BF8E00DC288B /* Assets.xcassets */, @@ -349,6 +363,7 @@ D59D89712B50572A0009270C /* Info.plist */, D59D894F2B4FFC380009270C /* main.swift */, D59D897F2B505EFE0009270C /* ManageClient.swift */, + D56116A92B510CFD00FEB087 /* DaemonManager.swift */, ); path = DotLocalHelperTool; sourceTree = ""; @@ -364,11 +379,13 @@ D59D895D2B5048420009270C /* Shared */ = { isa = PBXGroup; children = ( + D50377F72B481F69008F9AA8 /* Model */, D59D89602B5048C40009270C /* SharedConstants.swift */, D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */, D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */, D59D897C2B505E4B0009270C /* CodeInfo.swift */, D59D89822B5065CD0009270C /* MyError.swift */, + D5793C5F2B5103E300979DC3 /* DaemonState.swift */, ); path = Shared; sourceTree = ""; @@ -400,6 +417,7 @@ buildRules = ( ); dependencies = ( + D56116B12B51102F00FEB087 /* PBXTargetDependency */, ); name = DotLocal; packageProductDependencies = ( @@ -465,8 +483,9 @@ ); name = DotLocalHelperTool; packageProductDependencies = ( - D59D896F2B5055C00009270C /* EmbeddedPropertyList */, D59D897A2B505C4B0009270C /* SecureXPC */, + D5793C5B2B5103CC00979DC3 /* GRPC */, + D5793C5D2B5103CF00979DC3 /* EmbeddedPropertyList */, ); productName = DotLocalHelperTool; productReference = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; @@ -618,13 +637,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D59D89802B505EFE0009270C /* ManageClient.swift in Sources */, + D5793C602B5103E300979DC3 /* DaemonState.swift in Sources */, D59D897D2B505E4B0009270C /* CodeInfo.swift in Sources */, D5E8DD292B47E54800E083E0 /* AppDelegate.swift in Sources */, D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */, D59D89862B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */, - D50378092B48708C008F9AA8 /* MappingListViewModel.swift in Sources */, D5E8DD332B47E97F00E083E0 /* DaemonManager.swift in Sources */, D59D89612B5048C40009270C /* SharedConstants.swift in Sources */, D5DEA9B32B4888310029BB00 /* AppMenu.swift in Sources */, @@ -663,11 +681,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D56116B22B51109900FEB087 /* dot-local.grpc.swift in Sources */, + D56116B32B51109900FEB087 /* dot-local.pb.swift in Sources */, + D56116B42B51109900FEB087 /* preferences.pb.swift in Sources */, + D56116B52B51109900FEB087 /* ProtoExtensions.swift in Sources */, D59D89842B5065CD0009270C /* MyError.swift in Sources */, + D5793C612B5103E300979DC3 /* DaemonState.swift in Sources */, D59D897E2B505E4B0009270C /* CodeInfo.swift in Sources */, D59D89622B5048C40009270C /* SharedConstants.swift in Sources */, D59D89812B505EFE0009270C /* ManageClient.swift in Sources */, D59D89872B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, + D56116AA2B510CFD00FEB087 /* DaemonManager.swift in Sources */, D59D896A2B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */, D59D89502B4FFC380009270C /* main.swift in Sources */, ); @@ -686,6 +710,11 @@ target = D529B1A92B47BF8C00DC288B /* DotLocal */; targetProxy = D529B1C62B47BF8E00DC288B /* PBXContainerItemProxy */; }; + D56116B12B51102F00FEB087 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D59D894C2B4FFC380009270C /* DotLocalHelperTool */; + targetProxy = D56116B02B51102F00FEB087 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -948,6 +977,7 @@ INFOPLIST_FILE = "$(SRCROOT)/DotLocalHelperTool/Info.plist"; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(MARKETING_VERSION)"; + OTHER_SWIFT_FLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; @@ -964,6 +994,7 @@ INFOPLIST_FILE = "$(SRCROOT)/DotLocalHelperTool/Info.plist"; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(MARKETING_VERSION)"; + OTHER_SWIFT_FLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; @@ -1068,12 +1099,17 @@ package = D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */; productName = GRPC; }; - D59D896D2B5055BB0009270C /* EmbeddedPropertyList */ = { + D5793C5B2B5103CC00979DC3 /* GRPC */ = { + isa = XCSwiftPackageProductDependency; + package = D50377F42B481B59008F9AA8 /* XCRemoteSwiftPackageReference "grpc-swift" */; + productName = GRPC; + }; + D5793C5D2B5103CF00979DC3 /* EmbeddedPropertyList */ = { isa = XCSwiftPackageProductDependency; package = D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; productName = EmbeddedPropertyList; }; - D59D896F2B5055C00009270C /* EmbeddedPropertyList */ = { + D59D896D2B5055BB0009270C /* EmbeddedPropertyList */ = { isa = XCSwiftPackageProductDependency; package = D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */; productName = EmbeddedPropertyList; diff --git a/DotLocal/AppDelegate.swift b/DotLocal/AppDelegate.swift index 647f7e2..d8b8574 100644 --- a/DotLocal/AppDelegate.swift +++ b/DotLocal/AppDelegate.swift @@ -12,7 +12,6 @@ import SecureXPC class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { - DaemonManager.shared.start() ClientManager.shared.checkInstalled() _ = HelperManager.shared } @@ -29,8 +28,13 @@ 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") + Task { + await DaemonManager.shared.stop() + try? await HelperManager.shared.xpcClient.send(to: SharedConstants.exitRoute) + NSApplication.shared.terminate(nil) + } + return .terminateLater } } 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/ContentView.swift b/DotLocal/ContentView.swift index 320c8d9..b4eeb60 100644 --- a/DotLocal/ContentView.swift +++ b/DotLocal/ContentView.swift @@ -21,13 +21,21 @@ struct ContentView: View { switch daemonManager.state { case .stopped: Text("DotLocal is not running") - case .starting: + case .starting, .unknown: ProgressView() case .started: MappingList() } }.toolbar() { - StartStopButton(state: daemonManager.state, onStart: { daemonManager.start() }, onStop: { daemonManager.stop() }) + StartStopButton(state: daemonManager.state, onStart: { + Task { + await daemonManager.start() + } + }, onStop: { + Task { + await daemonManager.stop() + } + }) } default: Text("Unexpected state: \(helperManager.status.rawValue)") @@ -46,7 +54,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) { diff --git a/DotLocal/DaemonManager.swift b/DotLocal/DaemonManager.swift index 87b617a..1701c5a 100644 --- a/DotLocal/DaemonManager.swift +++ b/DotLocal/DaemonManager.swift @@ -13,106 +13,49 @@ 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 init() { } - func start() { - if state != .stopped { - return + func start() async { + do { + print("starting daemon") + try await HelperManager.shared.xpcClient.send(to: SharedConstants.startDaemonRoute) + print("successfully requested start") + } catch { + print("error starting daemon: \(error)") } - 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() - } - handle.waitForDataInBackgroundAndNotify() - } - 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 { - return + func subscribeDaemonState() async { + 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)") + } + if HelperManager.shared.status == .enabled { + Task { + await subscribeDaemonState() + } } - task.waitUntilExit() - } - - func updates() -> AnyPublisher { - return _updates.prepend(()).eraseToAnyPublisher() } } - -enum DaemonState { - case stopped - case starting - case started -} diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift index 8c44f5d..2f223a1 100644 --- a/DotLocal/HelperManager.swift +++ b/DotLocal/HelperManager.swift @@ -15,10 +15,10 @@ class HelperManager: ObservableObject { private let service = SMAppService.daemon(plistName: "helper.plist") @Published var status: SMAppService.Status - let xpcClient = XPCClient.forMachService(named: "dev.suphon.DotLocal.helper") + let xpcClient = XPCClient.forMachService(named: "dev.suphon.DotLocal.helper", withServerRequirement: try! .sameBundle) private init() { - status = service.status + status = .notRegistered Task { print("sending exit to current helper") do { @@ -37,8 +37,21 @@ class HelperManager: ObservableObject { } } + func onRegistered() async { + Task { + await DaemonManager.shared.subscribeDaemonState() + } + await DaemonManager.shared.start() + } + @MainActor func checkStatus() { + let oldStatus = status status = service.status + if oldStatus != .enabled && status == .enabled { + Task { + await onRegistered() + } + } } } diff --git a/DotLocal/MappingList.swift b/DotLocal/MappingList.swift index e95e0dc..49e96f3 100644 --- a/DotLocal/MappingList.swift +++ b/DotLocal/MappingList.swift @@ -9,10 +9,10 @@ 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 + List(daemonManager.mappings) { mapping in HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text("\(mapping.host)\(mapping.pathPrefix)") @@ -28,9 +28,7 @@ struct MappingList: View { .padding(.vertical, 4) } .overlay { - if vm.loading { - ProgressView() - } else if vm.mappings.isEmpty { + if daemonManager.mappings.isEmpty { if #available(macOS 14.0, *) { ContentUnavailableView { Label("No Routes", systemImage: "arrow.triangle.swap") @@ -49,6 +47,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 index 1a7947b..4e30c3c 100644 --- a/DotLocal/Model/ProtoExtensions.swift +++ b/DotLocal/Model/ProtoExtensions.swift @@ -9,6 +9,30 @@ 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)" diff --git a/DotLocalHelperTool/DaemonManager.swift b/DotLocalHelperTool/DaemonManager.swift new file mode 100644 index 0000000..7439242 --- /dev/null +++ b/DotLocalHelperTool/DaemonManager.swift @@ -0,0 +1,171 @@ +// +// 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 binUrl: URL + private let daemonUrl: URL + 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() { + let appUrl = try! parentAppURL() + binUrl = appUrl.appending(path: "Contents/Resources/bin") + daemonUrl = appUrl.appending(path: "Contents/Resources/dotlocal-daemon") + _internalStates + .flatMap { state in + Future { promise in + Task { + promise(.success(await DaemonManager.mapState(state))) + } + } + } + .subscribe(states) + .store(in: &subscriptions) + } + + func start() { + NSLog("received start") + 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/ManageClient.swift b/DotLocalHelperTool/ManageClient.swift index 670d43c..80a2b43 100644 --- a/DotLocalHelperTool/ManageClient.swift +++ b/DotLocalHelperTool/ManageClient.swift @@ -7,19 +7,6 @@ import Foundation -private func parentAppURL() throws -> URL { - let components = Bundle.main.bundleURL.pathComponents - guard let contentsIndex = components.lastIndex(of: "Contents"), - components[components.index(before: contentsIndex)].hasSuffix(".app") else { - throw MyError.runtimeError(""" - Parent bundle could not be found. - Path:\(Bundle.main.bundleURL) - """) - } - - return URL(fileURLWithPath: "/" + components[1.. URL { let appURL = try! parentAppURL() return appURL.appendingPathComponent("Contents/Resources/bin/dotlocal") diff --git a/DotLocalHelperTool/main.swift b/DotLocalHelperTool/main.swift index f102bee..372c6e5 100644 --- a/DotLocalHelperTool/main.swift +++ b/DotLocalHelperTool/main.swift @@ -8,13 +8,32 @@ import Foundation import SecureXPC +func parentAppURL() throws -> URL { + let components = Bundle.main.bundleURL.pathComponents + guard let contentsIndex = components.lastIndex(of: "Contents"), + components[components.index(before: contentsIndex)].hasSuffix(".app") else { + throw MyError.runtimeError(""" + Parent bundle could not be found. + Path:\(Bundle.main.bundleURL) + """) + } + + return URL(fileURLWithPath: "/" + components[1.. Date: Fri, 12 Jan 2024 15:48:19 +0700 Subject: [PATCH 10/32] chore: show ProgressView while getting status --- DotLocal/ContentView.swift | 10 ++++++---- DotLocal/HelperManager.swift | 7 +++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/DotLocal/ContentView.swift b/DotLocal/ContentView.swift index b4eeb60..d596bed 100644 --- a/DotLocal/ContentView.swift +++ b/DotLocal/ContentView.swift @@ -13,7 +13,8 @@ struct ContentView: View { @StateObject var helperManager = HelperManager.shared var body: some View { - switch helperManager.status { + let status = helperManager.status + switch status { case .requiresApproval: RequiresApprovalView() case .enabled: @@ -37,8 +38,10 @@ struct ContentView: View { } }) } + case nil: + ProgressView() default: - Text("Unexpected state: \(helperManager.status.rawValue)") + Text("Unexpected state: \(status!.rawValue)") } } } @@ -72,9 +75,8 @@ struct RequiresApprovalView: View { Text("Helper Not Enabled").font(.title).fontWeight(.bold) Text("Please enable DotLocal in the \"Allow in the Background\" section") Button(action: { - print("previous status: \(HelperManager.shared.status)") HelperManager.shared.checkStatus() - print("status: \(HelperManager.shared.status)") + print("user clicked continue, status: \(String(describing: HelperManager.shared.status))") if HelperManager.shared.status == .requiresApproval { openedSettings = true SMAppService.openSystemSettingsLoginItems() diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift index 2f223a1..4ea9428 100644 --- a/DotLocal/HelperManager.swift +++ b/DotLocal/HelperManager.swift @@ -13,12 +13,15 @@ class HelperManager: ObservableObject { static let shared = HelperManager() private let service = SMAppService.daemon(plistName: "helper.plist") - @Published var status: SMAppService.Status + @Published var status: SMAppService.Status? let xpcClient = XPCClient.forMachService(named: "dev.suphon.DotLocal.helper", withServerRequirement: try! .sameBundle) private init() { - status = .notRegistered + status = service.status + if status == .notFound || status == .notRegistered { + status = nil + } Task { print("sending exit to current helper") do { From 6ee42dd669d5a80ce62c22fd051c9eb35255fe3d Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:34:10 +0700 Subject: [PATCH 11/32] refactor: call dns-sd from go --- {c => dns-sd}/ClientCommon.c | 0 {c => dns-sd}/ClientCommon.h | 0 {c => dns-sd}/Makefile | 0 {c => dns-sd}/dns-sd.c | 46 +------- dns-sd/dns-sd.h | 8 ++ dns-sd/enums.go | 43 ++++++++ dns-sd/errors.go | 18 ++++ dns-sd/record.go | 21 ++++ dns-sd/service.go | 116 ++++++++++++++++++++ internal/daemon/dotlocal.go | 4 +- internal/daemon/mdnsproxy/controller.go | 137 +++++++----------------- 11 files changed, 250 insertions(+), 143 deletions(-) rename {c => dns-sd}/ClientCommon.c (100%) rename {c => dns-sd}/ClientCommon.h (100%) rename {c => dns-sd}/Makefile (100%) rename {c => dns-sd}/dns-sd.c (86%) create mode 100644 dns-sd/dns-sd.h create mode 100644 dns-sd/enums.go create mode 100644 dns-sd/errors.go create mode 100644 dns-sd/record.go create mode 100644 dns-sd/service.go diff --git a/c/ClientCommon.c b/dns-sd/ClientCommon.c similarity index 100% rename from c/ClientCommon.c rename to dns-sd/ClientCommon.c diff --git a/c/ClientCommon.h b/dns-sd/ClientCommon.h similarity index 100% rename from c/ClientCommon.h rename to dns-sd/ClientCommon.h diff --git a/c/Makefile b/dns-sd/Makefile similarity index 100% rename from c/Makefile rename to dns-sd/Makefile diff --git a/c/dns-sd.c b/dns-sd/dns-sd.c similarity index 86% rename from c/dns-sd.c rename to dns-sd/dns-sd.c index 1e113e4..fa52596 100644 --- a/c/dns-sd.c +++ b/dns-sd/dns-sd.c @@ -166,6 +166,8 @@ static const char kFilePathSep = '/'; #include "../mDNSShared/dnssd_clientstub.c" #endif +#include "dns-sd.h" + /** * Global */ @@ -249,14 +251,13 @@ static void getip(const char *const name, struct sockaddr_storage *result) if (addrs) freeaddrinfo(addrs); } -static DNSServiceErrorType RegisterProxyAddressRecord(DNSServiceRef sdref, const char *host, const char *ip, DNSServiceFlags flags) +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; - static DNSRecordRef record = NULL; memset(&hostaddr, 0, sizeof(hostaddr)); getip(ip, &hostaddr); if (!(flags & kDNSServiceFlagsShared)) @@ -264,10 +265,10 @@ static DNSServiceErrorType RegisterProxyAddressRecord(DNSServiceRef sdref, const flags |= kDNSServiceFlagsUnique; } if (hostaddr.ss_family == AF_INET) - return(DNSServiceRegisterRecord(sdref, &record, flags, kDNSServiceInterfaceIndexLocalOnly, host, + 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, &record, flags, kDNSServiceInterfaceIndexLocalOnly, host, + 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); } @@ -358,40 +359,3 @@ static void HandleEvents(void) } } #endif - -static bool isEndedWithDotLocal(const char *const hostname) { - const char *const dotLocal = ".local"; - const size_t hostnameLen = strlen(hostname); - const size_t dotLocalLen = strlen(dotLocal); - if (hostnameLen < dotLocalLen) return false; - return (strcasecmp(hostname + hostnameLen - dotLocalLen, dotLocal) == 0); -} - -int main(int argc, char *argv[]) { - if (argc < 2) { - printf("Usage: %s \n", argv[0]); - return 1; - } - DNSServiceErrorType err; - DNSServiceFlags flags = 0; - printtimestamp(); - printf("...STARTING...\n"); - err = DNSServiceCreateConnection(&client_pa); - if (err) { fprintf(stderr, "DNSServiceCreateConnection returned %d\n", err); return(err); } - for (int i = 1; i < argc; i++) { - char* host = argv[i]; - if (!isEndedWithDotLocal(host)) { - printf("Adding .local to %s\n", host); - host = malloc(strlen(argv[i]) + 7); - strcpy(host, argv[i]); - strcat(host, ".local"); - } - printf("Registering %s\n", host); - err = RegisterProxyAddressRecord(client_pa, host, "127.0.0.1", flags); - if (err) { fprintf(stderr, "DNSServiceRegisterRecord returned %d\n", err); return(err); } - } - HandleEvents(); - - if (client_pa) DNSServiceRefDeallocate(client_pa); - return 0; -} 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..7424729 --- /dev/null +++ b/dns-sd/service.go @@ -0,0 +1,116 @@ +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 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/internal/daemon/dotlocal.go b/internal/daemon/dotlocal.go index 67daeea..6270be8 100644 --- a/internal/daemon/dotlocal.go +++ b/internal/daemon/dotlocal.go @@ -34,7 +34,7 @@ func NewDotLocal(logger *zap.Logger) (*DotLocal, error) { return nil, err } - mdnsProxy, err := mdnsproxy.NewMDNSProxy(logger.Named("dnsproxy")) + dnsProxy, err := mdnsproxy.NewMDNSProxy(logger.Named("dnsproxy")) if err != nil { return nil, err } @@ -42,7 +42,7 @@ func NewDotLocal(logger *zap.Logger) (*DotLocal, error) { return &DotLocal{ logger: logger, nginx: nginx, - dnsProxy: mdnsProxy, + dnsProxy: dnsProxy, mappings: make(map[internal.MappingKey]*internal.MappingState), }, nil } diff --git a/internal/daemon/mdnsproxy/controller.go b/internal/daemon/mdnsproxy/controller.go index ba03b9e..0226e49 100644 --- a/internal/daemon/mdnsproxy/controller.go +++ b/internal/daemon/mdnsproxy/controller.go @@ -1,145 +1,82 @@ package mdnsproxy import ( - "bufio" - "os" - "os/exec" + "context" + dnssd "github.com/softnetics/dotlocal/dns-sd" "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 MDNSProxy struct { logger *zap.Logger - port int - nginxConfigFile string - command *exec.Cmd + dnsService dnssd.DNSService + registeredHosts map[string]dnssd.DNSRecord + + cancelProcess context.CancelFunc } func NewMDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { - nginxConfigFile, err := util.CreateTmpFile() - if err != nil { - return nil, err - } - return &MDNSProxy{ logger: logger, - nginxConfigFile: nginxConfigFile, + registeredHosts: make(map[string]dnssd.DNSRecord), }, nil } func (p *MDNSProxy) Start() error { - p.logger.Debug("Ensuring nginx image exists", zap.String("image", nginxImage)) - err := p.writeNginxConfig() + 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(context.Background()) + 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") - var t tomb.Tomb - t.Go(func() error { - return os.Remove(p.nginxConfigFile) - }) - return t.Wait() + 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)) - hosts := make([]string, len(hostsMap)) - i := 0 for host := range hostsMap { - hosts[i] = host - i++ - } - - p.logger.Debug("Setting hosts", zap.Any("hosts", hosts)) - cmd := exec.Command("./cmd/dns-sd/dns-sd", hosts...) - stdout, err := cmd.StdoutPipe() - if err != nil { - p.logger.Error("Failed to get stdout pipe", zap.Error(err)) - return err - } - err = cmd.Start() - if err != nil { - p.logger.Error("Failed to start dns-sd", zap.Error(err)) - return err - } - go func() { - scanner := bufio.NewScanner(stdout) - for scanner.Scan() { - p.logger.Info("dns-sd", zap.String("line", scanner.Text())) + if _, ok := p.registeredHosts[host]; ok { + continue } - p.logger.Info("dns-sd exited") - }() - if p.command != nil { - err := p.command.Process.Kill() + 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 } - p.command = cmd - return nil -} -func (p *MDNSProxy) 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_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 + 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 } From f5ebbf4c484665bf8e5ebc90b11a4273c75f46e6 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:57:06 +0700 Subject: [PATCH 12/32] fix: startup status check --- DotLocal/HelperManager.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift index 4ea9428..c9f15af 100644 --- a/DotLocal/HelperManager.swift +++ b/DotLocal/HelperManager.swift @@ -14,6 +14,7 @@ class HelperManager: ObservableObject { private let service = SMAppService.daemon(plistName: "helper.plist") @Published var status: SMAppService.Status? + private var ranRegistered = false let xpcClient = XPCClient.forMachService(named: "dev.suphon.DotLocal.helper", withServerRequirement: try! .sameBundle) @@ -49,9 +50,9 @@ class HelperManager: ObservableObject { @MainActor func checkStatus() { - let oldStatus = status status = service.status - if oldStatus != .enabled && status == .enabled { + if status == .enabled && !ranRegistered { + ranRegistered = true Task { await onRegistered() } From b46222eef312448db96dc3665bec41cad1954bae Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:57:25 +0700 Subject: [PATCH 13/32] feat: compile with CGO_ENABLED --- DotLocal.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index a841d06..dab9d79 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -594,7 +594,7 @@ ); 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"; }; D59D89722B505A550009270C /* ShellScript */ = { isa = PBXShellScriptBuildPhase; From bc2f52d8291c5acd887593494dbc54914b83b88a Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:14:18 +0700 Subject: [PATCH 14/32] feat: make sure host ends with .local --- DotLocal.xcodeproj/project.pbxproj | 81 +++++++++---------- .../Model/ProtoExtensions.swift | 0 .../Model/proto/dot-local.grpc.swift | 22 ++--- .../Model/proto/dot-local.pb.swift | 0 .../Model/proto/preferences.pb.swift | 0 internal/api/proto/dot-local.pb.go | 27 +++---- internal/api/proto/dot-local_grpc.pb.go | 10 +-- internal/client/cmd/root.go | 4 +- internal/daemon/apiserver.go | 28 ++++--- internal/daemon/dotlocal.go | 4 + proto/dot-local.proto | 2 +- 11 files changed, 92 insertions(+), 86 deletions(-) rename {DotLocal => Shared}/Model/ProtoExtensions.swift (100%) rename {DotLocal => Shared}/Model/proto/dot-local.grpc.swift (95%) rename {DotLocal => Shared}/Model/proto/dot-local.pb.swift (100%) rename {DotLocal => Shared}/Model/proto/preferences.pb.swift (100%) diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index dab9d79..d9032b3 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -8,11 +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 */; }; 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,10 +17,14 @@ D529B1CA2B47BF8E00DC288B /* DotLocalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1C92B47BF8E00DC288B /* DotLocalUITests.swift */; }; D529B1CC2B47BF8E00DC288B /* DotLocalUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B1CB2B47BF8E00DC288B /* DotLocalUITestsLaunchTests.swift */; }; D56116AA2B510CFD00FEB087 /* DaemonManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56116A92B510CFD00FEB087 /* DaemonManager.swift */; }; - D56116B22B51109900FEB087 /* dot-local.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378012B482578008F9AA8 /* dot-local.grpc.swift */; }; - D56116B32B51109900FEB087 /* dot-local.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378022B482578008F9AA8 /* dot-local.pb.swift */; }; - D56116B42B51109900FEB087 /* preferences.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50378032B482578008F9AA8 /* preferences.pb.swift */; }; - D56116B52B51109900FEB087 /* ProtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D503780C2B487250008F9AA8 /* ProtoExtensions.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 */; }; 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 */; }; @@ -152,11 +152,7 @@ /* 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 = ""; }; 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 = ""; }; @@ -169,6 +165,10 @@ 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 = ""; }; D5793C482B50E8D400979DC3 /* HelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperManager.swift; sourceTree = ""; }; D5793C5F2B5103E300979DC3 /* DaemonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaemonState.swift; sourceTree = ""; }; D582E9052B4C5BC00054343B /* nginx */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = nginx; sourceTree = ""; }; @@ -236,26 +236,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - D50377F72B481F69008F9AA8 /* Model */ = { - isa = PBXGroup; - children = ( - D50378072B482588008F9AA8 /* proto */, - D503780C2B487250008F9AA8 /* ProtoExtensions.swift */, - ); - name = Model; - path = ../DotLocal/Model; - sourceTree = ""; - }; - D50378072B482588008F9AA8 /* proto */ = { - isa = PBXGroup; - children = ( - D50378012B482578008F9AA8 /* dot-local.grpc.swift */, - D50378022B482578008F9AA8 /* dot-local.pb.swift */, - D50378032B482578008F9AA8 /* preferences.pb.swift */, - ); - path = proto; - sourceTree = ""; - }; D529B1A12B47BF8C00DC288B = { isa = PBXGroup; children = ( @@ -331,6 +311,25 @@ 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 = ""; + }; D582E8FF2B4C5AEE0054343B /* bin */ = { isa = PBXGroup; children = ( @@ -379,7 +378,7 @@ D59D895D2B5048420009270C /* Shared */ = { isa = PBXGroup; children = ( - D50377F72B481F69008F9AA8 /* Model */, + D56116B62B51621500FEB087 /* Model */, D59D89602B5048C40009270C /* SharedConstants.swift */, D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */, D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */, @@ -638,8 +637,10 @@ buildActionMask = 2147483647; files = ( D5793C602B5103E300979DC3 /* DaemonState.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 */, D59D89862B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */, @@ -647,15 +648,13 @@ D59D89612B5048C40009270C /* SharedConstants.swift in Sources */, D5DEA9B32B4888310029BB00 /* AppMenu.swift in Sources */, D59D89832B5065CD0009270C /* MyError.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 */, - D50378062B482578008F9AA8 /* preferences.pb.swift in Sources */, + D56116BC2B51621500FEB087 /* dot-local.pb.swift in Sources */, D5DEA9C42B49C0200029BB00 /* ClientManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -681,18 +680,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D56116B22B51109900FEB087 /* dot-local.grpc.swift in Sources */, - D56116B32B51109900FEB087 /* dot-local.pb.swift in Sources */, - D56116B42B51109900FEB087 /* preferences.pb.swift in Sources */, - D56116B52B51109900FEB087 /* ProtoExtensions.swift in Sources */, D59D89842B5065CD0009270C /* MyError.swift in Sources */, 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 */, + D56116C42B51628E00FEB087 /* ProtoExtensions.swift in Sources */, D56116AA2B510CFD00FEB087 /* DaemonManager.swift in Sources */, D59D896A2B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */, + D56116BD2B51621500FEB087 /* dot-local.pb.swift in Sources */, D59D89502B4FFC380009270C /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DotLocal/Model/ProtoExtensions.swift b/Shared/Model/ProtoExtensions.swift similarity index 100% rename from DotLocal/Model/ProtoExtensions.swift rename to Shared/Model/ProtoExtensions.swift 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/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..465330e 100644 --- a/internal/client/cmd/root.go +++ b/internal/client/cmd/root.go @@ -43,7 +43,7 @@ var ( 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,7 +54,7 @@ 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 } timer := time.NewTimer(duration) diff --git a/internal/daemon/apiserver.go b/internal/daemon/apiserver.go index fae7ded..3ea066d 100644 --- a/internal/daemon/apiserver.go +++ b/internal/daemon/apiserver.go @@ -123,8 +123,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, @@ -132,7 +132,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) { @@ -149,16 +149,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/dotlocal.go b/internal/daemon/dotlocal.go index 6270be8..5daac63 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" @@ -178,6 +179,9 @@ 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 = "/" } 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) {} } From e947513b21ae81ece1fdad06bc560972fb3aa1b7 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:28:51 +0700 Subject: [PATCH 15/32] fix: don't fail on interrupt while processing --- dns-sd/service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dns-sd/service.go b/dns-sd/service.go index 7424729..2c3684c 100644 --- a/dns-sd/service.go +++ b/dns-sd/service.go @@ -68,6 +68,9 @@ func (s *dnsService) Process(ctx context.Context) error { 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 } From cd1324c68eb01d0123a4360aea753de8d7e8170b Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:31:12 +0700 Subject: [PATCH 16/32] fix: stop nginx with context --- internal/daemon/apiserver.go | 2 +- internal/daemon/dnsproxy/interface.go | 4 +++- internal/daemon/dotlocal.go | 8 ++++---- internal/daemon/main.go | 8 ++++---- internal/daemon/mdnsproxy/controller.go | 4 ++-- internal/daemon/nginx.go | 8 +++++++- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/daemon/apiserver.go b/internal/daemon/apiserver.go index 3ea066d..735980a 100644 --- a/internal/daemon/apiserver.go +++ b/internal/daemon/apiserver.go @@ -31,7 +31,7 @@ func NewAPIServer(logger *zap.Logger, dotlocal *DotLocal) (*APIServer, error) { }, nil } -func (s *APIServer) Start() error { +func (s *APIServer) Start(ctx context.Context) error { err := s.killExistingProcess() if err != nil { return err diff --git a/internal/daemon/dnsproxy/interface.go b/internal/daemon/dnsproxy/interface.go index 1a5052c..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() 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 5daac63..2b6e814 100644 --- a/internal/daemon/dotlocal.go +++ b/internal/daemon/dotlocal.go @@ -48,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 @@ -75,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() + return d.dnsProxy.Start(ctx) }) err = t.Wait() 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 index 0226e49..8c340d3 100644 --- a/internal/daemon/mdnsproxy/controller.go +++ b/internal/daemon/mdnsproxy/controller.go @@ -25,7 +25,7 @@ func NewMDNSProxy(logger *zap.Logger) (dnsproxy.DNSProxy, error) { }, nil } -func (p *MDNSProxy) Start() error { +func (p *MDNSProxy) Start(ctx context.Context) error { p.logger.Debug("Connecting to dns service") service, err := dnssd.NewConnection() if err != nil { @@ -34,7 +34,7 @@ func (p *MDNSProxy) Start() error { p.dnsService = service p.logger.Info("Ready") - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) p.cancelProcess = cancel go func() { err := service.Process(ctx) diff --git a/internal/daemon/nginx.go b/internal/daemon/nginx.go index bd2b311..bf6fcf0 100644 --- a/internal/daemon/nginx.go +++ b/internal/daemon/nginx.go @@ -2,6 +2,7 @@ package daemon import ( "bufio" + "context" "errors" "fmt" "io" @@ -39,7 +40,7 @@ func NewNginx(logger *zap.Logger) (*Nginx, error) { }, nil } -func (n *Nginx) Start() error { +func (n *Nginx) Start(ctx context.Context) error { n.writeConfig() n.logger.Debug("Starting nginx") @@ -69,6 +70,11 @@ 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 From f98d120562c5605785ad1030c0e223f4bf3bbf72 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:37:40 +0700 Subject: [PATCH 17/32] feat: terminate daemon before exiting helper --- DotLocalHelperTool/main.swift | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/DotLocalHelperTool/main.swift b/DotLocalHelperTool/main.swift index 372c6e5..6e8ce13 100644 --- a/DotLocalHelperTool/main.swift +++ b/DotLocalHelperTool/main.swift @@ -7,6 +7,7 @@ import Foundation import SecureXPC +import Dispatch func parentAppURL() throws -> URL { let components = Bundle.main.bundleURL.pathComponents @@ -34,10 +35,7 @@ if getppid() == 1 { server.registerRoute(SharedConstants.installClientRoute, handler: ManageClient.install) server.registerRoute(SharedConstants.uninstallClientRoute, handler: ManageClient.uninstall) - server.registerRoute(SharedConstants.exitRoute, handler: { - NSLog("exiting") - exit(0) - }) + server.registerRoute(SharedConstants.exitRoute, handler: gracefulExit) server.setErrorHandler { error in if case .connectionInvalid = error { // Ignore invalidated connections as this happens whenever the client disconnects which is not a problem @@ -45,7 +43,24 @@ if getppid() == 1 { 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 { print("not supported") } + +func gracefulExit() { + NSLog("exiting") + DaemonManager.shared.stop() + exit(0) +} From 9d10f39a56e58c33632fab379f1a06935a069b1f Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:52:37 +0700 Subject: [PATCH 18/32] feat: try killing nginx on start --- internal/daemon/apiserver.go | 3 +++ internal/daemon/nginx.go | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/internal/daemon/apiserver.go b/internal/daemon/apiserver.go index 735980a..62e86e0 100644 --- a/internal/daemon/apiserver.go +++ b/internal/daemon/apiserver.go @@ -39,6 +39,9 @@ func (s *APIServer) Start(ctx context.Context) error { 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) diff --git a/internal/daemon/nginx.go b/internal/daemon/nginx.go index bf6fcf0..9aa4a86 100644 --- a/internal/daemon/nginx.go +++ b/internal/daemon/nginx.go @@ -8,6 +8,8 @@ import ( "io" "os" "os/exec" + "path" + "strconv" "strings" "sync" "syscall" @@ -41,6 +43,11 @@ func NewNginx(logger *zap.Logger) (*Nginx, error) { } func (n *Nginx) Start(ctx context.Context) error { + err := n.killExistingProcess() + if err != nil { + return err + } + n.writeConfig() n.logger.Debug("Starting nginx") @@ -79,6 +86,7 @@ func (n *Nginx) Start(ctx context.Context) error { if err != nil { return err } + wg.Wait() if !nginxStarted { err := cmd.Wait() @@ -235,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 +} From 6869db6e4427943dfc9bf6aa63cb2fa208eec617 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 21:07:21 +0700 Subject: [PATCH 19/32] feat: show route count --- DotLocal.xcodeproj/project.pbxproj | 6 ++++++ DotLocal/MappingList.swift | 12 ++++++++++-- DotLocal/ViewExtensions.swift | 24 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 DotLocal/ViewExtensions.swift diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index d9032b3..652cc55 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 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 */; }; @@ -169,6 +171,7 @@ 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 = ""; }; D582E9052B4C5BC00054343B /* nginx */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = nginx; sourceTree = ""; }; @@ -282,6 +285,7 @@ D5DEA9C32B49C0200029BB00 /* ClientManager.swift */, D5DEA9BD2B4995DD0029BB00 /* SettingsView.swift */, D5DEA9BF2B499C2F0029BB00 /* Defaults.swift */, + D56116C52B517DC800FEB087 /* ViewExtensions.swift */, ); path = DotLocal; sourceTree = ""; @@ -637,6 +641,7 @@ 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 */, @@ -688,6 +693,7 @@ 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 */, diff --git a/DotLocal/MappingList.swift b/DotLocal/MappingList.swift index 49e96f3..15f6bd2 100644 --- a/DotLocal/MappingList.swift +++ b/DotLocal/MappingList.swift @@ -12,7 +12,8 @@ struct MappingList: View { @StateObject var daemonManager = DaemonManager.shared var body: some View { - List(daemonManager.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,8 +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 daemonManager.mappings.isEmpty { + if mappings.isEmpty { if #available(macOS 14.0, *) { ContentUnavailableView { Label("No Routes", systemImage: "arrow.triangle.swap") 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 + } + } +} From 2579eb1eb94cedd4efe8cc92a291276a351efa54 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 21:07:59 +0700 Subject: [PATCH 20/32] feat: build on all branches --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 6a9e28cf8d6a42b293da950809455982bf415fd5 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Fri, 12 Jan 2024 22:38:35 +0700 Subject: [PATCH 21/32] feat: log bundle on helper start --- DotLocal.xcodeproj/project.pbxproj | 6 ++++++ DotLocal/DaemonManager.swift | 11 ++++++++++- DotLocal/HelperManager.swift | 3 --- DotLocalHelperTool/main.swift | 14 +------------- Shared/Utils.swift | 28 ++++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 Shared/Utils.swift diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index 652cc55..05162c4 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 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 */; }; + D5803EC72B518F2600332743 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EC62B518F2600332743 /* Utils.swift */; }; + D5803EC82B518F2600332743 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EC62B518F2600332743 /* Utils.swift */; }; D582E9072B4C5BE20054343B /* nginx in Copy Binaries */ = {isa = PBXBuildFile; fileRef = D582E9052B4C5BC00054343B /* nginx */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D59D89502B4FFC380009270C /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D894F2B4FFC380009270C /* main.swift */; }; D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */ = {isa = PBXBuildFile; fileRef = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -174,6 +176,7 @@ 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 = ""; }; + D5803EC62B518F2600332743 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.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 = ""; }; @@ -389,6 +392,7 @@ D59D897C2B505E4B0009270C /* CodeInfo.swift */, D59D89822B5065CD0009270C /* MyError.swift */, D5793C5F2B5103E300979DC3 /* DaemonState.swift */, + D5803EC62B518F2600332743 /* Utils.swift */, ); path = Shared; sourceTree = ""; @@ -647,6 +651,7 @@ D5E8DD292B47E54800E083E0 /* AppDelegate.swift in Sources */, D56116C32B51628E00FEB087 /* ProtoExtensions.swift in Sources */, D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */, + D5803EC72B518F2600332743 /* Utils.swift in Sources */, D59D89862B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, D5793C492B50E8D400979DC3 /* HelperManager.swift in Sources */, D5E8DD332B47E97F00E083E0 /* DaemonManager.swift in Sources */, @@ -690,6 +695,7 @@ D59D897E2B505E4B0009270C /* CodeInfo.swift in Sources */, D59D89622B5048C40009270C /* SharedConstants.swift in Sources */, D56116C12B51621500FEB087 /* dot-local.grpc.swift in Sources */, + D5803EC82B518F2600332743 /* Utils.swift in Sources */, D56116BF2B51621500FEB087 /* preferences.pb.swift in Sources */, D59D89812B505EFE0009270C /* ManageClient.swift in Sources */, D59D89872B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, diff --git a/DotLocal/DaemonManager.swift b/DotLocal/DaemonManager.swift index 1701c5a..eee3c0a 100644 --- a/DotLocal/DaemonManager.swift +++ b/DotLocal/DaemonManager.swift @@ -16,6 +16,8 @@ class DaemonManager: ObservableObject { @Published var state: DaemonState = .unknown @Published var mappings: [Mapping] = [] + private var subscribing = false + private init() { } @@ -24,6 +26,9 @@ class DaemonManager: ObservableObject { print("starting daemon") try await HelperManager.shared.xpcClient.send(to: SharedConstants.startDaemonRoute) print("successfully requested start") + Task { + await subscribeDaemonState() + } } catch { print("error starting daemon: \(error)") } @@ -39,7 +44,11 @@ class DaemonManager: ObservableObject { } } - func subscribeDaemonState() async { + private func subscribeDaemonState() async { + if subscribing { + return + } + subscribing = true do { for try await state in HelperManager.shared.xpcClient.send(to: SharedConstants.daemonStateRoute) { DispatchQueue.main.async { diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift index c9f15af..f9f35ff 100644 --- a/DotLocal/HelperManager.swift +++ b/DotLocal/HelperManager.swift @@ -42,9 +42,6 @@ class HelperManager: ObservableObject { } func onRegistered() async { - Task { - await DaemonManager.shared.subscribeDaemonState() - } await DaemonManager.shared.start() } diff --git a/DotLocalHelperTool/main.swift b/DotLocalHelperTool/main.swift index 6e8ce13..63f77e6 100644 --- a/DotLocalHelperTool/main.swift +++ b/DotLocalHelperTool/main.swift @@ -9,21 +9,9 @@ import Foundation import SecureXPC import Dispatch -func parentAppURL() throws -> URL { - let components = Bundle.main.bundleURL.pathComponents - guard let contentsIndex = components.lastIndex(of: "Contents"), - components[components.index(before: contentsIndex)].hasSuffix(".app") else { - throw MyError.runtimeError(""" - Parent bundle could not be found. - Path:\(Bundle.main.bundleURL) - """) - } - - return URL(fileURLWithPath: "/" + components[1.. URL { + return try parentAppURL(Bundle.main.bundleURL.pathComponents) +} + +func parentAppURL(_ bundleURL: URL) throws -> URL { + return try parentAppURL(bundleURL.pathComponents) +} + +func parentAppURL(_ components: [String]) throws -> URL { + guard let contentsIndex = components.lastIndex(of: "Contents"), + components[components.index(before: contentsIndex)].hasSuffix(".app") else { + throw MyError.runtimeError(""" + Parent bundle could not be found. + Path:\(Bundle.main.bundleURL) + """) + } + + return URL(fileURLWithPath: "/" + components[1.. Date: Sat, 13 Jan 2024 00:50:52 +0700 Subject: [PATCH 22/32] refactor: migrate from SMAppService --- DotLocal.xcodeproj/project.pbxproj | 259 +++++++-- .../xcshareddata/swiftpm/Package.resolved | 27 + DotLocal/AppConfig.xcconfig | 4 + DotLocal/AppDelegate.swift | 15 +- DotLocal/ClientManager.swift | 2 +- DotLocal/ContentView.swift | 100 ++-- DotLocal/DaemonManager.swift | 7 +- DotLocal/HelperManager.swift | 55 +- DotLocal/HelperToolMonitor.swift | 134 +++++ DotLocal/Info.plist | 11 + DotLocal/LaunchDaemons/helper.plist | 21 - DotLocalHelperTool/DaemonManager.swift | 22 +- DotLocalHelperTool/HelperToolConfig.xcconfig | 8 +- DotLocalHelperTool/Info.plist | 11 +- DotLocalHelperTool/ManageClient.swift | 19 +- DotLocalHelperTool/Uninstaller.swift | 124 ++++ DotLocalHelperTool/Updater.swift | 67 +++ DotLocalHelperTool/launchd.plist | 13 +- DotLocalHelperTool/main.swift | 30 +- PropertyListModifier/main.swift | 547 ++++++++++++++++++ Shared/MyError.swift | 12 - Shared/SharedConstants.swift | 83 +++ Shared/Utils.swift | 28 - 23 files changed, 1381 insertions(+), 218 deletions(-) create mode 100644 DotLocal/HelperToolMonitor.swift create mode 100644 DotLocal/Info.plist delete mode 100644 DotLocal/LaunchDaemons/helper.plist create mode 100644 DotLocalHelperTool/Uninstaller.swift create mode 100644 DotLocalHelperTool/Updater.swift create mode 100644 PropertyListModifier/main.swift delete mode 100644 Shared/MyError.swift delete mode 100644 Shared/Utils.swift diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index 05162c4..eb2125e 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -32,12 +32,15 @@ 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 */; }; - D5803EC72B518F2600332743 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EC62B518F2600332743 /* Utils.swift */; }; - D5803EC82B518F2600332743 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5803EC62B518F2600332743 /* Utils.swift */; }; - D582E9072B4C5BE20054343B /* nginx in Copy Binaries */ = {isa = PBXBuildFile; fileRef = D582E9052B4C5BC00054343B /* nginx */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + 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 */; }; - D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */ = {isa = PBXBuildFile; fileRef = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - D59D895C2B4FFF080009270C /* helper.plist in Copy Helper Plist */ = {isa = PBXBuildFile; fileRef = D59D89562B4FFD290009270C /* helper.plist */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 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 */; }; @@ -48,8 +51,6 @@ 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 */; }; - D59D89832B5065CD0009270C /* MyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89822B5065CD0009270C /* MyError.swift */; }; - D59D89842B5065CD0009270C /* MyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59D89822B5065CD0009270C /* MyError.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 */; }; @@ -86,9 +87,32 @@ 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; @@ -101,10 +125,10 @@ D59D89592B4FFDD30009270C /* Copy Helper */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; - dstPath = Contents/Library/LaunchDaemons; + dstPath = Contents/Library/LaunchServices; dstSubfolderSpec = 1; files = ( - D59D895A2B4FFDE20009270C /* DotLocalHelperTool in Copy Helper */, + D5803EDE2B519ADA00332743 /* dev.suphon.DotLocal.helper in Copy Helper */, ); name = "Copy Helper"; runOnlyForDeploymentPostprocessing = 0; @@ -115,7 +139,6 @@ dstPath = Contents/Library/LaunchDaemons; dstSubfolderSpec = 1; files = ( - D59D895C2B4FFF080009270C /* helper.plist in Copy Helper Plist */, ); name = "Copy Helper Plist"; runOnlyForDeploymentPostprocessing = 0; @@ -142,15 +165,15 @@ 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 */ @@ -176,13 +199,16 @@ 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 = ""; }; - D5803EC62B518F2600332743 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.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 /* DotLocalHelperTool */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = DotLocalHelperTool; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; - D59D89562B4FFD290009270C /* helper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = helper.plist; 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 = ""; }; @@ -190,7 +216,6 @@ 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 = ""; }; - D59D89822B5065CD0009270C /* MyError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyError.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 = ""; }; @@ -210,6 +235,7 @@ D5DEA9BC2B4995BC0029BB00 /* Defaults in Frameworks */, D5DEA9B62B49936B0029BB00 /* LaunchAtLogin in Frameworks */, D59D89792B505C430009270C /* SecureXPC in Frameworks */, + D5803EE22B519E3B00332743 /* Blessed in Frameworks */, D50377F62B481B59008F9AA8 /* GRPC in Frameworks */, D59D896E2B5055BB0009270C /* EmbeddedPropertyList in Frameworks */, ); @@ -229,11 +255,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D5803ECA2B5194CF00332743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; D59D894A2B4FFC380009270C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D5793C5C2B5103CC00979DC3 /* GRPC in Frameworks */, + D5803EE42B519E4600332743 /* Blessed in Frameworks */, D59D897B2B505C4B0009270C /* SecureXPC in Frameworks */, D5793C5E2B5103CF00979DC3 /* EmbeddedPropertyList in Frameworks */, ); @@ -253,6 +287,7 @@ D529B1AC2B47BF8C00DC288B /* DotLocal */, D59D894E2B4FFC380009270C /* DotLocalHelperTool */, D529B1C82B47BF8E00DC288B /* DotLocalUITests */, + D5803ECE2B5194CF00332743 /* PropertyListModifier */, D529B1AB2B47BF8C00DC288B /* Products */, D59D896C2B5055BB0009270C /* Frameworks */, ); @@ -264,7 +299,8 @@ D529B1AA2B47BF8C00DC288B /* DotLocal.app */, D529B1BB2B47BF8E00DC288B /* DotLocalTests.xctest */, D529B1C52B47BF8E00DC288B /* DotLocalUITests.xctest */, - D59D894D2B4FFC380009270C /* DotLocalHelperTool */, + D59D894D2B4FFC380009270C /* dev.suphon.DotLocal.helper */, + D5803ECD2B5194CF00332743 /* PropertyListModifier */, ); name = Products; sourceTree = ""; @@ -273,22 +309,22 @@ isa = PBXGroup; children = ( D59D89632B50506D0009270C /* AppConfig.xcconfig */, - D59D89552B4FFCFF0009270C /* LaunchDaemons */, D529B1AD2B47BF8C00DC288B /* DotLocalApp.swift */, D5E8DD282B47E54800E083E0 /* AppDelegate.swift */, D5DEA9B22B4888310029BB00 /* AppMenu.swift */, + D5803EDC2B51990100332743 /* HelperToolMonitor.swift */, D529B1AF2B47BF8C00DC288B /* ContentView.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 */, D56116C52B517DC800FEB087 /* ViewExtensions.swift */, + D529B1BE2B47BF8E00DC288B /* DotLocalTests */, + D529B1B12B47BF8E00DC288B /* Assets.xcassets */, + D529B1B62B47BF8E00DC288B /* DotLocal.entitlements */, + D529B1B32B47BF8E00DC288B /* Preview Content */, ); path = DotLocal; sourceTree = ""; @@ -337,6 +373,14 @@ path = proto; sourceTree = ""; }; + D5803ECE2B5194CF00332743 /* PropertyListModifier */ = { + isa = PBXGroup; + children = ( + D5803ECF2B5194CF00332743 /* main.swift */, + ); + path = PropertyListModifier; + sourceTree = ""; + }; D582E8FF2B4C5AEE0054343B /* bin */ = { isa = PBXGroup; children = ( @@ -370,18 +414,12 @@ D59D894F2B4FFC380009270C /* main.swift */, D59D897F2B505EFE0009270C /* ManageClient.swift */, D56116A92B510CFD00FEB087 /* DaemonManager.swift */, + D5803EE62B51A19500332743 /* Uninstaller.swift */, + D5803EE82B51A1A200332743 /* Updater.swift */, ); path = DotLocalHelperTool; sourceTree = ""; }; - D59D89552B4FFCFF0009270C /* LaunchDaemons */ = { - isa = PBXGroup; - children = ( - D59D89562B4FFD290009270C /* helper.plist */, - ); - path = LaunchDaemons; - sourceTree = ""; - }; D59D895D2B5048420009270C /* Shared */ = { isa = PBXGroup; children = ( @@ -390,9 +428,7 @@ D59D89682B50548C0009270C /* HelperToolInfoPropertyList.swift */, D59D89852B5067E60009270C /* HelperToolLaunchdPropertyList.swift */, D59D897C2B505E4B0009270C /* CodeInfo.swift */, - D59D89822B5065CD0009270C /* MyError.swift */, D5793C5F2B5103E300979DC3 /* DaemonState.swift */, - D5803EC62B518F2600332743 /* Utils.swift */, ); path = Shared; sourceTree = ""; @@ -411,19 +447,22 @@ 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 */, + D5803ED92B51962300332743 /* Cleanup Job Bless Requirements */, ); buildRules = ( ); dependencies = ( + D5803ED72B5194FA00332743 /* PBXTargetDependency */, D56116B12B51102F00FEB087 /* PBXTargetDependency */, ); name = DotLocal; @@ -433,6 +472,7 @@ D5DEA9BB2B4995BC0029BB00 /* Defaults */, D59D896D2B5055BB0009270C /* EmbeddedPropertyList */, D59D89782B505C430009270C /* SecureXPC */, + D5803EE12B519E3B00332743 /* Blessed */, ); productName = DotLocal; productReference = D529B1AA2B47BF8C00DC288B /* DotLocal.app */; @@ -474,6 +514,23 @@ 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" */; @@ -487,15 +544,17 @@ buildRules = ( ); dependencies = ( + D5803ED52B5194F100332743 /* PBXTargetDependency */, ); name = DotLocalHelperTool; packageProductDependencies = ( D59D897A2B505C4B0009270C /* SecureXPC */, D5793C5B2B5103CC00979DC3 /* GRPC */, D5793C5D2B5103CF00979DC3 /* EmbeddedPropertyList */, + D5803EE32B519E4600332743 /* Blessed */, ); productName = DotLocalHelperTool; - productReference = D59D894D2B4FFC380009270C /* DotLocalHelperTool */; + productReference = D59D894D2B4FFC380009270C /* dev.suphon.DotLocal.helper */; productType = "com.apple.product-type.tool"; }; /* End PBXNativeTarget section */ @@ -519,6 +578,9 @@ CreatedOnToolsVersion = 15.1; TestTargetID = D529B1A92B47BF8C00DC288B; }; + D5803ECC2B5194CF00332743 = { + CreatedOnToolsVersion = 15.1; + }; D59D894C2B4FFC380009270C = { CreatedOnToolsVersion = 15.1; }; @@ -539,6 +601,8 @@ D5DEA9BA2B4995BC0029BB00 /* XCRemoteSwiftPackageReference "Defaults" */, D59D896B2B5054FE0009270C /* XCRemoteSwiftPackageReference "EmbeddedPropertyList" */, D59D89772B505C430009270C /* XCRemoteSwiftPackageReference "SecureXPC" */, + D5803EE02B519E3B00332743 /* XCRemoteSwiftPackageReference "Blessed" */, + D5803EE52B519E5100332743 /* XCRemoteSwiftPackageReference "Authorized" */, ); productRefGroup = D529B1AB2B47BF8C00DC288B /* Products */; projectDirPath = ""; @@ -548,6 +612,7 @@ D59D894C2B4FFC380009270C /* DotLocalHelperTool */, D529B1C42B47BF8E00DC288B /* DotLocalUITests */, D529B1BA2B47BF8E00DC288B /* DotLocalTests */, + D5803ECC2B5194CF00332743 /* PropertyListModifier */, ); }; /* End PBXProject section */ @@ -603,6 +668,42 @@ 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)\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 = "$TARGET_BUILD_DIR/PropertyListModifier satisfy-job-bless-requirements\n"; + }; + D5803ED92B51962300332743 /* Cleanup Job Bless Requirements */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Cleanup Job Bless Requirements"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# $TARGET_BUILD_DIR/PropertyListModifier satisfy-job-bless-requirements\n"; + }; D59D89722B505A550009270C /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -618,7 +719,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "LAUNCHDPLIST_FILE=$PROJECT_DIR/$LAUNCHDPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleIdentifier\" $INFOPLIST_FILE || true\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleVersion\" $INFOPLIST_FILE || true\nrm $LAUNCHDPLIST_FILE\n\n/usr/libexec/PlistBuddy -c \"Add :CFBundleIdentifier string $HELPER_TOOL_BUNDLE_IDENTIFIER\" $INFOPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Add :CFBundleVersion string $HELPER_VERSION\" $INFOPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Add :MachServices array\" $LAUNCHDPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Add :MachServices: string $HELPER_TOOL_BUNDLE_IDENTIFIER\" $LAUNCHDPLIST_FILE\n"; + shellScript = "$TARGET_BUILD_DIR/PropertyListModifier satisfy-job-bless-requirements specify-mach-services\n"; }; D59D89732B505B260009270C /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -635,7 +736,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "LAUNCHDPLIST_FILE=$PROJECT_DIR/$LAUNCHDPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleIdentifier\" $INFOPLIST_FILE\n/usr/libexec/PlistBuddy -c \"Delete :CFBundleVersion\" $INFOPLIST_FILE\n#rm $LAUNCHDPLIST_FILE\n"; + shellScript = "# $TARGET_BUILD_DIR/PropertyListModifier cleanup-job-bless-requirements cleanup-mach-services\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -651,13 +752,12 @@ D5E8DD292B47E54800E083E0 /* AppDelegate.swift in Sources */, D56116C32B51628E00FEB087 /* ProtoExtensions.swift in Sources */, D5DEA9C02B499C2F0029BB00 /* Defaults.swift in Sources */, - D5803EC72B518F2600332743 /* Utils.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 */, - D59D89832B5065CD0009270C /* MyError.swift in Sources */, D56116C02B51621500FEB087 /* dot-local.grpc.swift in Sources */, D529B1B02B47BF8C00DC288B /* ContentView.swift in Sources */, D59D89692B50548C0009270C /* HelperToolInfoPropertyList.swift in Sources */, @@ -686,16 +786,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D5803EC92B5194CF00332743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5803ED02B5194CF00332743 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D59D89492B4FFC380009270C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D59D89842B5065CD0009270C /* MyError.swift in Sources */, D5793C612B5103E300979DC3 /* DaemonState.swift in Sources */, D59D897E2B505E4B0009270C /* CodeInfo.swift in Sources */, D59D89622B5048C40009270C /* SharedConstants.swift in Sources */, D56116C12B51621500FEB087 /* dot-local.grpc.swift in Sources */, - D5803EC82B518F2600332743 /* Utils.swift in Sources */, D56116BF2B51621500FEB087 /* preferences.pb.swift in Sources */, D59D89812B505EFE0009270C /* ManageClient.swift in Sources */, D59D89872B5067E60009270C /* HelperToolLaunchdPropertyList.swift in Sources */, @@ -703,8 +809,10 @@ 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; }; @@ -726,6 +834,16 @@ 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 */ @@ -977,6 +1095,26 @@ }; name = Release; }; + D5803ED12B5194CF00332743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + MACOSX_DEPLOYMENT_TARGET = 14.2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D5803ED22B5194CF00332743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + MACOSX_DEPLOYMENT_TARGET = 14.2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; D59D89512B4FFC380009270C /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D59D89642B5050E60009270C /* HelperToolConfig.xcconfig */; @@ -989,7 +1127,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(MARKETING_VERSION)"; OTHER_SWIFT_FLAGS = ""; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "$(HELPER_TOOL_BUNDLE_IDENTIFIER)"; SWIFT_VERSION = 5.0; }; name = Debug; @@ -1006,7 +1144,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(MARKETING_VERSION)"; OTHER_SWIFT_FLAGS = ""; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "$(HELPER_TOOL_BUNDLE_IDENTIFIER)"; SWIFT_VERSION = 5.0; }; name = Release; @@ -1050,6 +1188,15 @@ 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 = ( @@ -1070,6 +1217,22 @@ 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"; @@ -1120,6 +1283,16 @@ 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" */; diff --git a/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DotLocal.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 42471cb..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", @@ -36,6 +54,15 @@ "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", diff --git a/DotLocal/AppConfig.xcconfig b/DotLocal/AppConfig.xcconfig index 8269549..3c8635a 100644 --- a/DotLocal/AppConfig.xcconfig +++ b/DotLocal/AppConfig.xcconfig @@ -10,5 +10,9 @@ #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 d8b8574..1c62451 100644 --- a/DotLocal/AppDelegate.swift +++ b/DotLocal/AppDelegate.swift @@ -13,7 +13,6 @@ import SecureXPC class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { ClientManager.shared.checkInstalled() - _ = HelperManager.shared } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { @@ -30,11 +29,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { print("applicationShouldTerminate called, stopping daemon and helper") - Task { - await DaemonManager.shared.stop() - try? await HelperManager.shared.xpcClient.send(to: SharedConstants.exitRoute) - NSApplication.shared.terminate(nil) + 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 } - return .terminateLater } } diff --git a/DotLocal/ClientManager.swift b/DotLocal/ClientManager.swift index 3579284..970122c 100644 --- a/DotLocal/ClientManager.swift +++ b/DotLocal/ClientManager.swift @@ -18,7 +18,7 @@ class ClientManager: ObservableObject { func installCli() async { do { - try await HelperManager.shared.xpcClient.send(to: SharedConstants.installClientRoute) + try await HelperManager.shared.xpcClient.sendMessage(Bundle.main.bundleURL, to: SharedConstants.installClientRoute) checkInstalled() } catch { print("error installing cli: \(error)") diff --git a/DotLocal/ContentView.swift b/DotLocal/ContentView.swift index d596bed..2e91d42 100644 --- a/DotLocal/ContentView.swift +++ b/DotLocal/ContentView.swift @@ -7,41 +7,41 @@ 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.status - switch status { - case .requiresApproval: - RequiresApprovalView() - case .enabled: - 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() + let status = helperManager.installationStatus + VStack { + 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() } - case nil: - ProgressView() - default: - Text("Unexpected state: \(status!.rawValue)") } } } @@ -67,35 +67,41 @@ struct StartStopButton: View { } } -struct RequiresApprovalView: View { - @State var openedSettings = false +struct RequiresHelperView: View { + @State private var didError = false + @State private var errorMessage = "" var body: some View { VStack(spacing: 8) { - Text("Helper Not Enabled").font(.title).fontWeight(.bold) - Text("Please enable DotLocal in the \"Allow in the Background\" section") + Text("Helper Not Installed").font(.title).fontWeight(.bold) + Text("Please install the helper in order to use DotLocal") Button(action: { - HelperManager.shared.checkStatus() - print("user clicked continue, status: \(String(describing: HelperManager.shared.status))") - if HelperManager.shared.status == .requiresApproval { - openedSettings = true - SMAppService.openSystemSettingsLoginItems() + do { + try PrivilegedHelperManager.shared + .authorizeAndBless(message: nil) + } catch AuthorizationError.canceled { + // No user feedback needed, user canceled + } catch { + errorMessage = error.localizedDescription + didError = true } }, label: { - if openedSettings { - Text("Continue") - } else { - Text("Open System Settings") - } + Text("Install Helper") }) - }.foregroundStyle(.secondary) + } + .foregroundStyle(.secondary) + .alert( + "Install failed", + isPresented: $didError, + presenting: errorMessage + ) { _ in + Button("OK") {} + } message: { message in + Text(message) + } } } -//#Preview { -// ContentView() -//} - #Preview { - RequiresApprovalView() + ContentView() } diff --git a/DotLocal/DaemonManager.swift b/DotLocal/DaemonManager.swift index eee3c0a..57f44c9 100644 --- a/DotLocal/DaemonManager.swift +++ b/DotLocal/DaemonManager.swift @@ -24,7 +24,7 @@ class DaemonManager: ObservableObject { func start() async { do { print("starting daemon") - try await HelperManager.shared.xpcClient.send(to: SharedConstants.startDaemonRoute) + try await HelperManager.shared.xpcClient.sendMessage(Bundle.main.bundleURL, to: SharedConstants.startDaemonRoute) print("successfully requested start") Task { await subscribeDaemonState() @@ -61,10 +61,5 @@ class DaemonManager: ObservableObject { } catch { print("error during state subscription: \(error)") } - if HelperManager.shared.status == .enabled { - Task { - await subscribeDaemonState() - } - } } } diff --git a/DotLocal/HelperManager.swift b/DotLocal/HelperManager.swift index f9f35ff..8eff568 100644 --- a/DotLocal/HelperManager.swift +++ b/DotLocal/HelperManager.swift @@ -12,46 +12,45 @@ import SecureXPC class HelperManager: ObservableObject { static let shared = HelperManager() - private let service = SMAppService.daemon(plistName: "helper.plist") - @Published var status: SMAppService.Status? - private var ranRegistered = false + private let monitor = HelperToolMonitor(constants: SharedConstants.shared) + @Published var installationStatus: HelperToolMonitor.InstallationStatus + private var started = false - let xpcClient = XPCClient.forMachService(named: "dev.suphon.DotLocal.helper", withServerRequirement: try! .sameBundle) + let xpcClient = XPCClient.forMachService(named: SharedConstants.shared.machServiceName) private init() { - status = service.status - if status == .notFound || status == .notRegistered { - status = nil + installationStatus = monitor.determineStatus() + monitor.start { status in + DispatchQueue.main.async { + self.updateStatus(status: status) + } } Task { - print("sending exit to current helper") - do { - try await xpcClient.send(to: SharedConstants.exitRoute) - } catch { - print("error sending exit: \(error)") - } - do { - print("registering service") - try service.register() - print("registered service") - } catch { - print("error registering service: \(error)") + if installationStatus.isReady { + try await updateHelper() } - await checkStatus() } } - func onRegistered() async { - await DaemonManager.shared.start() + 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 + } } - @MainActor - func checkStatus() { - status = service.status - if status == .enabled && !ranRegistered { - ranRegistered = true + private func updateStatus(status: HelperToolMonitor.InstallationStatus) { + installationStatus = status + if status.isReady, !started { + started = true Task { - await onRegistered() + await DaemonManager.shared.start() } } } diff --git a/DotLocal/HelperToolMonitor.swift b/DotLocal/HelperToolMonitor.swift new file mode 100644 index 0000000..16413c0 --- /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(let bundleVersion) = 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/LaunchDaemons/helper.plist b/DotLocal/LaunchDaemons/helper.plist deleted file mode 100644 index d0ce49f..0000000 --- a/DotLocal/LaunchDaemons/helper.plist +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Label - dev.suphon.DotLocal.helper - BundleProgram - Contents/Library/LaunchDaemons/DotLocalHelperTool - MachServices - - dev.suphon.DotLocal.helper - - - RunAtLoad - - StandardOutPath - /tmp/dotlocal.out - StandardErrPath - /tmp/dotlocal.err - - diff --git a/DotLocalHelperTool/DaemonManager.swift b/DotLocalHelperTool/DaemonManager.swift index 7439242..329c382 100644 --- a/DotLocalHelperTool/DaemonManager.swift +++ b/DotLocalHelperTool/DaemonManager.swift @@ -14,8 +14,6 @@ import SecureXPC class DaemonManager { static let shared = DaemonManager() - private let binUrl: URL - private let daemonUrl: URL private let runDirectory = URL.init(filePath: "/var/run/dotlocal") var internalState: DaemonStateInternal = .stopped @@ -30,9 +28,6 @@ class DaemonManager { private var subscriptions = Set() private init() { - let appUrl = try! parentAppURL() - binUrl = appUrl.appending(path: "Contents/Resources/bin") - daemonUrl = appUrl.appending(path: "Contents/Resources/dotlocal-daemon") _internalStates .flatMap { state in Future { promise in @@ -45,15 +40,26 @@ class DaemonManager { .store(in: &subscriptions) } - func start() { + 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) + let binPath = binURL.path(percentEncoded: false) + let launchPath = daemonURL.path(percentEncoded: false) try! FileManager.default.createDirectory(at: runDirectory, withIntermediateDirectories: true) diff --git a/DotLocalHelperTool/HelperToolConfig.xcconfig b/DotLocalHelperTool/HelperToolConfig.xcconfig index 62396c0..fd4fabd 100644 --- a/DotLocalHelperTool/HelperToolConfig.xcconfig +++ b/DotLocalHelperTool/HelperToolConfig.xcconfig @@ -13,6 +13,13 @@ // 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 @@ -22,5 +29,4 @@ LAUNCHDPLIST_FILE = $(TARGET_DIRECTORY)/launchd.plist // 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) -PRODUCT_BUNDLE_IDENTIFIER = $(HELPER_TOOL_BUNDLE_IDENTIFIER) SWIFT_ACTIVE_COMPILATION_CONDITIONS = HELPER_TOOL diff --git a/DotLocalHelperTool/Info.plist b/DotLocalHelperTool/Info.plist index 0c67376..4edc715 100644 --- a/DotLocalHelperTool/Info.plist +++ b/DotLocalHelperTool/Info.plist @@ -1,5 +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 index 80a2b43..4f2b819 100644 --- a/DotLocalHelperTool/ManageClient.swift +++ b/DotLocalHelperTool/ManageClient.swift @@ -7,20 +7,21 @@ import Foundation -private func clientLocation() -> URL { - let appURL = try! parentAppURL() - return appURL.appendingPathComponent("Contents/Resources/bin/dotlocal") -} - private let installLocation = URL.init(filePath: "/usr/local/bin/dotlocal") enum ManageClient { - static func install() throws { - let source = clientLocation() + static func install(bundleURL: URL) throws { + let clientURL = bundleURL.appending(path: "Contents/Resources/bin/dotlocal") NSLog("installing client") - NSLog("symlink \(source) to \(installLocation)") + + 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: source) + try FileManager.default.createSymbolicLink(at: installLocation, withDestinationURL: clientURL) NSLog("installed 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 index b702d49..276292b 100644 --- a/DotLocalHelperTool/launchd.plist +++ b/DotLocalHelperTool/launchd.plist @@ -2,9 +2,18 @@ - MachServices + AssociatedBundleIdentifiers - dev.suphon.DotLocal.helper + 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 index 63f77e6..a1a3469 100644 --- a/DotLocalHelperTool/main.swift +++ b/DotLocalHelperTool/main.swift @@ -11,10 +11,24 @@ import Dispatch NSLog("starting helper tool. PID \(getpid()). PPID \(getppid()).") NSLog("version: \(try HelperToolInfoPropertyList.main.version.rawValue)") -NSLog("bundle: \(String(describing: try? parentAppURL()))") +NSLog("code location: \(String(describing: try? CodeInfo.currentCodeLocation()))") -if getppid() == 1 { - let server = try XPCServer.forMachService(withCriteria: .forDaemon(withClientRequirement: try! .sameParentBundle)) +// 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) @@ -24,6 +38,10 @@ if getppid() == 1 { 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 @@ -43,8 +61,10 @@ if getppid() == 1 { sigtermSrc.resume() server.startAndBlock() -} else { - print("not supported") +} 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() { 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 { - return try parentAppURL(Bundle.main.bundleURL.pathComponents) -} - -func parentAppURL(_ bundleURL: URL) throws -> URL { - return try parentAppURL(bundleURL.pathComponents) -} - -func parentAppURL(_ components: [String]) throws -> URL { - guard let contentsIndex = components.lastIndex(of: "Contents"), - components[components.index(before: contentsIndex)].hasSuffix(".app") else { - throw MyError.runtimeError(""" - Parent bundle could not be found. - Path:\(Bundle.main.bundleURL) - """) - } - - return URL(fileURLWithPath: "/" + components[1.. Date: Sat, 13 Jan 2024 00:53:46 +0700 Subject: [PATCH 23/32] chore: ignore profraw --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 70894da6adbd325f6e1480e6cb888c25b78e1fdb Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Sat, 13 Jan 2024 00:54:14 +0700 Subject: [PATCH 24/32] fix: use created mapping as delete key --- internal/client/cmd/root.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/client/cmd/root.go b/internal/client/cmd/root.go index 465330e..72d0339 100644 --- a/internal/client/cmd/root.go +++ b/internal/client/cmd/root.go @@ -38,6 +38,8 @@ var ( exitCode := 0 + var createdMapping *api.Mapping + loopCtx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { @@ -57,6 +59,7 @@ var ( 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) }, From 9e0e317eedd626f4bcd72ffa3152160c460eb84c Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:02:12 +0700 Subject: [PATCH 25/32] fix: plsit modifier path --- DotLocal.xcodeproj/project.pbxproj | 41 ++---------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index eb2125e..584cced 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -457,7 +457,6 @@ D5E8DD2F2B47E82200E083E0 /* Copy Client */, D59D89592B4FFDD30009270C /* Copy Helper */, D59D895B2B4FFEFB0009270C /* Copy Helper Plist */, - D5803ED92B51962300332743 /* Cleanup Job Bless Requirements */, ); buildRules = ( ); @@ -539,7 +538,6 @@ D59D89492B4FFC380009270C /* Sources */, D59D894A2B4FFC380009270C /* Frameworks */, D59D894B2B4FFC380009270C /* CopyFiles */, - D59D89732B505B260009270C /* ShellScript */, ); buildRules = ( ); @@ -684,25 +682,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "$TARGET_BUILD_DIR/PropertyListModifier satisfy-job-bless-requirements\n"; - }; - D5803ED92B51962300332743 /* Cleanup Job Bless Requirements */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Cleanup Job Bless Requirements"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# $TARGET_BUILD_DIR/PropertyListModifier satisfy-job-bless-requirements\n"; + shellScript = "$BUILT_PRODUCTS_DIR/PropertyListModifier satisfy-job-bless-requirements\n"; }; D59D89722B505A550009270C /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -719,24 +699,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "$TARGET_BUILD_DIR/PropertyListModifier satisfy-job-bless-requirements specify-mach-services\n"; - }; - D59D89732B505B260009270C /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# $TARGET_BUILD_DIR/PropertyListModifier cleanup-job-bless-requirements cleanup-mach-services\n"; + shellScript = "$BUILT_PRODUCTS_DIR/PropertyListModifier satisfy-job-bless-requirements specify-mach-services\n"; }; /* End PBXShellScriptBuildPhase section */ From 3bea18b1a93eaed9b39019f5117aea1529747f37 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:03:08 +0700 Subject: [PATCH 26/32] chore: unused variable --- DotLocal/HelperToolMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotLocal/HelperToolMonitor.swift b/DotLocal/HelperToolMonitor.swift index 16413c0..62658ff 100644 --- a/DotLocal/HelperToolMonitor.swift +++ b/DotLocal/HelperToolMonitor.swift @@ -38,7 +38,7 @@ class HelperToolMonitor { var isReady: Bool { get { - if registeredWithLaunchd, registrationPropertyListExists, case .exists(let bundleVersion) = helperToolExecutable { + if registeredWithLaunchd, registrationPropertyListExists, case .exists(_) = helperToolExecutable { return true } else { return false From b7e37b3c9b7d745f59511d4f649a2a40e7aea2f9 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:32:31 +0700 Subject: [PATCH 27/32] fix: plist modifier deployment target --- DotLocal.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DotLocal.xcodeproj/project.pbxproj b/DotLocal.xcodeproj/project.pbxproj index 584cced..e36c22d 100644 --- a/DotLocal.xcodeproj/project.pbxproj +++ b/DotLocal.xcodeproj/project.pbxproj @@ -1062,7 +1062,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - MACOSX_DEPLOYMENT_TARGET = 14.2; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; @@ -1072,7 +1072,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - MACOSX_DEPLOYMENT_TARGET = 14.2; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; From d7708c9af21be75da413b12de430b164f7001ba4 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Sat, 13 Jan 2024 01:44:55 +0700 Subject: [PATCH 28/32] fix: startup crashes --- DotLocal/AppDelegate.swift | 3 ++- internal/daemon/apiserver.go | 24 +++++++++++++----------- internal/util/util.go | 12 +++++++++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/DotLocal/AppDelegate.swift b/DotLocal/AppDelegate.swift index 1c62451..ceb8bc8 100644 --- a/DotLocal/AppDelegate.swift +++ b/DotLocal/AppDelegate.swift @@ -11,7 +11,8 @@ import Defaults import SecureXPC class AppDelegate: NSObject, NSApplicationDelegate { - func applicationDidFinishLaunching(_ notification: Notification) { + override init() { + _ = HelperManager.shared ClientManager.shared.checkInstalled() } diff --git a/internal/daemon/apiserver.go b/internal/daemon/apiserver.go index 62e86e0..1273937 100644 --- a/internal/daemon/apiserver.go +++ b/internal/daemon/apiserver.go @@ -32,7 +32,7 @@ func NewAPIServer(logger *zap.Logger, dotlocal *DotLocal) (*APIServer, error) { } func (s *APIServer) Start(ctx context.Context) error { - err := s.killExistingProcess() + err := s.killExistingProcessIfNeeded() if err != nil { return err } @@ -75,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) { @@ -85,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)) @@ -98,17 +107,10 @@ func (s *APIServer) killExistingProcess() error { if err != nil { return err } - _ = process.Kill() - - err = os.Remove(util.GetPidPath()) + err = process.Kill() if err != nil { return err } - err = os.Remove(util.GetApiSocketPath()) - if err != nil { - return err - } - return nil } diff --git a/internal/util/util.go b/internal/util/util.go index 22a2d46..55db165 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -18,7 +18,17 @@ func CreateTmpFile() (string, error) { var dotlocalPath *string func GetDotlocalPath() string { - return "/var/run/dotlocal" + if dotlocalPath == nil { + dir := "/var/run/dotlocal" + dotlocalPath = &dir + + err := os.MkdirAll(dir, 0755) + if err != nil { + panic(err) + } + } + + return *dotlocalPath } func GetApiSocketPath() string { From 5e27b98a6d1007350a1d2ef06a721d117af20c9d Mon Sep 17 00:00:00 2001 From: Suphon T <8080853+suphon-t@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:08:51 +0700 Subject: [PATCH 29/32] chore: changeset --- .changeset/old-cheetahs-watch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/old-cheetahs-watch.md 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 From 7da08f67fa9de56ece53e5fe458ca8dc145e9689 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:09:26 +0700 Subject: [PATCH 30/32] chore: remove unused binary --- cmd/dns-sd/dns-sd | Bin 35496 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 cmd/dns-sd/dns-sd diff --git a/cmd/dns-sd/dns-sd b/cmd/dns-sd/dns-sd deleted file mode 100755 index ab7e08fbfbed00c5afa1e3bc08217333ef49acd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35496 zcmeHQeQ;FO6~Avcfz9$2J|vK6vMGiHgzzCk5(3?jgfB7B4GLAtW3&5`tZa6}?gmJs zWycJfjMm9hIx>zeL7nM_S_V7SZk$%)Wb9z4(+;#%>sT9*+C`+JW2enhL;5@Sy-nUe zq}Fyi(?9NI&b#;Ak8{ucopbM9X1Ir6U%hf|GGh`$vVdlQ;u(w`W)74Xn+w_qlI3kx z8*1;Zb#0~E^u!0JS6#=EoGx^ff{m7ib#e z)7M+GR+Hz|a8M%h#^%WZ)JzJOQu1sa2#PnQ?_swQuzE`)fq484hGWQU_5S+1$t zUL{v=t*h6p;nd_XZ~e%BY+~We_FuF(T4;8t4|#jncux zjII2f#;K@tfJn}gws2^P-`B9j+wS**r<4aGJxWge!S-kSitOW$UH$Ccp*!2Kjrcfi zO^{-l%GbxSldf48qMfe~Ii(V;kL1w)GbWn13`A|TW@$|swzo8{3-qaP4@Z=i(siZK z%h!NTNb^A?KL&g#U!e1Z7bU`i>a)Ay-$vl6Q6zzYKtLcM5D*9m1Ox&C0fB%(Kp-Fx z5C{ka1Ofs9fq+0jARrJB2nYlO0s;YnfIvVXAP^7;2m}NI0s(=5KtLcM5D*9m1Ox&C z0fB%(Kp-Fx_|gcBR!e6`Ix_l3tde?qupr*sSr9*NEf|h<%5hs3+n4ysv0S_pS%rVX zJu7CFhi#HGdCAJu5{V@-Sr5xDSAS<=M@m@K6~p-b=y$p^KW<~}l&#R2Wa!_<*b(0M zv2yhn=<7fmc=b_~dk6F5vBBMOn}oS?j^!rwx%l{7wDEmtPgt`~q~?V#J%zt4SC3&l zjiq^aVqUI8b$*=dTD?2oM{AI>_R<=fZR2a`tNvcx*6tflOk>HRY0l)6&~Lz!-pIQ(0*Z(lgr||`76{$)R7(u$WQ6bG3csLyU~V! zkAI4=Igj?|I(O64f}V9}D%7LUVQ>#b9B%Gzp?j?bXrvS%<#}WNgc{z zNr^?zt;5)%9B1;lRXTe4^+z<9Jb*KR2EYWl4 z=dmQmp=-6e9%IfIIFlFVIg`AfgQ*)(XJeY5Y}D=5+bIuod}%#yrY2^xB>5({LwvP5 zo8-(Kmtn;`7p&G38K~#$!uVL{uJ}h*+lg59uJ~ojt-InMBJUf#Z_MxT-Y9unCQB~N zg8V7E{NdH=HHjS=>Db%{JTzRe=8+C--^d_Sd$Et8Z;!2|LM318h40M7yhHGrGr%b^ zktGxG8{1siU=@5{w}B0N33i!56#KNG46i%*-n&G7rL0Hs2<8*MKKL0HI);gH46QBp zsa%Gq95Y5bs{2N?1}heT6VIX_3xr^KrH*ibR-im?}A z+XUvYPlw%5COa+!&i4GM3pn@V$ym?k7jIQlm={9d!d|9YS#-vHVDu;JgcFY-KLovo zcCdc5YxA|kNB-4(g*#zsi!ymJ-2fM7% zbDJd=vP|!O8uokw2Nlr-?s*c|q&1=+uU>-UPIGrdCvas$vEpD%*wlj{k2(& zx)wZW&x0>cx-^sQkTnVZ+YetN%=o%u`nqD7Q5Uswyr_M|x-aq8vD_54G={L^b7nC$ z2K=aRiV}yM<(MP65ZHbQdzDDQ6+HZCc!4CT{Jbh2KiA2w1tYBdbZ5aZ-iFTV#_tH~ zUtk!=#{l~-T}SGhy1U4Bd=2pRGpRL@e_;NUZlo*SL!={}PexzETf$S>7VIxe!JE81 z!BDmv%2N&H8HVy~LwUZTywFfCHI!Ex$`yw4T0?oAp}gKu-fSq>8Okn0`5r_0UPF1e zq1<37HyX|JFBUyuUA6*eIBJcq_`tWbubW6JP}_o;0P&^wopLvI*Pm)=MQ?^e&$k|AXK3@ z)tJK0mMvdZx)gtw83d*JxD-z?kMz6YbzX!7cVXf<6~z<%e@sv zOZ87_(bC(!4hL&eB5to2^ECz?N+=Wz(fCHn!mO6_o}gFZDsBiy9Bxe4>Tl;gLfZUA z;R*+m8Ngn0=T`zL2 zQ+AZz;8XnsJ)fxOIIobhqgi^cJ-g42(z|7^w z`#}$bo&^nnE`lr;CQSw9gO-A}fDqJ7dI)p`bQ1I`=vSckKz{?-txU?*A{8+C#$hFl zZ7!v;MsfQov_wQnZ_uJNXdxtwE%i#I#usjNM?B46Yg5`VN|sxc7Ef!t>}lRhTsY$P z>?KBSbo>12HBD{qkXNhnBAP_2UJSNH^kN8_j0*~-i46tDS+A~x+~W2HE>x0W2boD1;Ox70!amhS(OIiTGNSV58UF&SYQM>uVwrJnRNF$RUQw z2rJ}DOISf;N^Cd+y~B#91*<^0k^1wJUkUI$KuzQjGUKrttn7t0M$sj#>|-y!8}<)Vb>6sO^AQ$ev6 zm;zGI>iobR8vlXL?^&twa4+8fvsD_Ot@9hpG(Jb?7nN)LGU6%NMO5P}b>61)^*TRC z=Mj^S_b<_T1is?@Ql0;<&X?&tST+qhzCe2gWr~9_P12%a5Pz%Q-nv|CUkD!c)Wp+z ze2u(p;@eI9cTD_qCjKQ8PiKKq{%=hDdnW!P6MxmjXKBL5^v^T#i0_p)-%=A_X5y<% z{8kgc%f#czNs}Ko@lg}sW#W5G{81DCyoo<);$JrL6#s;*O>s}i$0^$8-6oKZ0ia{kH#ByWgE4P7eGzY~}p*SkEFOBn2%#{Cj zP6w0Ib~cX6>vlFj!Hg%YIS!DqO&r*1r}zK(z}C)pngn-RORKz|?uFWbSMl!fMVf1Z zku5wXlbi~;v@H}o*j|NbNlG}Jx;Zx@%8_5G-LC{9VSdT@a7WzLo*JTctH!<3fLoKv z9={Jgfq*P~R6#Y8t4U6oy88&*k zJ$WT}+U(Zd#rHh<_>~`YR9(xMz3Fz=x$?s1w_m?)m947jTa`DTeEnqa Date: Mon, 15 Jan 2024 13:57:24 +0700 Subject: [PATCH 31/32] fix: add default_server back --- internal/daemon/nginx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/daemon/nginx.go b/internal/daemon/nginx.go index 9aa4a86..06d409e 100644 --- a/internal/daemon/nginx.go +++ b/internal/daemon/nginx.go @@ -214,7 +214,7 @@ func (n *Nginx) writeConfig() error { Directives: []gonginx.IDirective{ &gonginx.Directive{ Name: "listen", - Parameters: []string{"127.0.0.1"}, + Parameters: []string{"127.0.0.1", "default_server"}, }, &gonginx.Directive{ Name: "return", From 9b5fd183ab6b7eb0c7116475391988d4aa5deb67 Mon Sep 17 00:00:00 2001 From: Suphon Thanakornpakapong <8080853+suphon-t@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:29:16 +0700 Subject: [PATCH 32/32] fix: ensure target starts with http or https --- internal/daemon/dotlocal.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/daemon/dotlocal.go b/internal/daemon/dotlocal.go index 2b6e814..844b8d9 100644 --- a/internal/daemon/dotlocal.go +++ b/internal/daemon/dotlocal.go @@ -185,6 +185,9 @@ func (d *DotLocal) CreateMapping(opts internal.MappingOptions) (internal.Mapping 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,