diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift index a41812b..3ca4b1f 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/HelloManager.swift @@ -56,21 +56,21 @@ actor HelloManager: Dialer { private func startTailscale() async { do { - /// This sets up a localAPI client attached to the local node. + // This sets up a localAPI client attached to the local node. let node = try setupNode() try await node.up() - let localAPIClient = LocalAPIClient(localNode: node, logger: logger) - // Once we have our local node, we can set up the local API client. + // Create a localAPIClient instance for our local node + let localAPIClient = LocalAPIClient(localNode: node, logger: logger) setLocalAPIClient(localAPIClient) - setReady(true) - /// This sets up a bus watcher to listen for changes in the netmap. These will be sent to the given consumer, uin - /// this case, a HelloModel which will keep track of the changes and publish them. - if let processor = await localAPIClient.watchIPNBus(mask: [.initialState, .netmap, .rateLimitNetmaps, .noPrivateKeys], - consumer: model) { - setProcessor(processor) - } + // This sets up a bus watcher to listen for changes in the netmap. These will be sent to the given consumer, in + // this case, a HelloModel which will keep track of the changes and publish them. + let busEventMask: Ipn.NotifyWatchOpt = [.initialState, .netmap, .rateLimitNetmaps, .noPrivateKeys] + let processor = try await localAPIClient.watchIPNBus(mask: busEventMask , + consumer: model) + setProcessor(processor) + setReady(true) } catch { Logger().log("Error setting up Tailscale: \(error)") setReady(false) @@ -102,7 +102,7 @@ actor HelloManager: Dialer { return } - await setMessage("Phoning " + Settings.tailnetServer + "...") + await setMessage("Phoning " + Settings.tailnetURL + "...") // Create a URLSession that can access nodes on the tailnet. // .tailscaleSession(node) is the magic sauce. This sends your URLRequest via @@ -111,12 +111,11 @@ actor HelloManager: Dialer { let session = URLSession(configuration: sessionConfig) // Request a resource from the tailnet... - let url = URL(string: Settings.tailnetServer)! - var req = URLRequest(url: url) - + let url = URL(string: Settings.tailnetURL)! + let req = URLRequest(url: url) let (data, _) = try await session.data(for: req) - await setMessage("\(Settings.tailnetServer) says:\n \(String(data: data, encoding: .utf8) ?? "(crickets!)")") + await setMessage("\(Settings.tailnetURL) says:\n \(String(data: data, encoding: .utf8) ?? "(crickets!)")") } catch { await setMessage("Whoops!: \(error)") } diff --git a/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift b/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift index 6c7d0ea..ad656bd 100644 --- a/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift +++ b/swift/Examples/TailscaleKitHello/HelloFromTailscale/TailnetSettings.swift @@ -8,7 +8,7 @@ struct Settings { static let authKey = "tskey-auth-your-auth-key" // Note: The sample has a transport exception for http on ts.net so http:// is ok... // The "Phone Home" button will load the contents of this URL, it should be on your Tailnet. - static let tailnetServer = "http://myserver.my-tailnet.ts.net" + static let tailnetURL = "http://myserver.my-tailnet.ts.net" // Identifies this application in the Tailscale admin console. static let hostName = "Hello-From-Tailsacle-Sample-App" } diff --git a/swift/Makefile b/swift/Makefile index 41abce4..9fe8167 100644 --- a/swift/Makefile +++ b/swift/Makefile @@ -10,8 +10,13 @@ endif # The xcodebuild schemes will run the Makefile in the root directory to build # the libtailscale.a and libtailscale_ios.a dependencies. +.PHONY: all +all: test ios macos ## Runs the tests and builds all library targets + .PHONY: macos macos: ## Builds TailscaleKit for macos to swift/build/Build/Products/Release (unsigned) + @echo + @echo "::: Building TailscaleKit for macOS :::" cd .. && make c-archive mkdir -p build xcodebuild build -scheme "TailscaleKit (macOS)" \ @@ -22,6 +27,8 @@ macos: ## Builds TailscaleKit for macos to swift/build/Build/Products/Release ( .PHONY: ios ios: ## Builds TailscaleKit for iOS to swift/build/Build/Products/Release (unsigned) + @echo + @echo "::: Building TailscaleKit for iOS :::" cd .. && make c-archive-ios mkdir -p build xcodebuild build -scheme "TailscaleKit (iOS)" \ @@ -33,6 +40,8 @@ ios: ## Builds TailscaleKit for iOS to swift/build/Build/Products/Release (unsi .PHONY: test test: ## Run tests (macOS) + @echo + @echo "::: Running tests for TailscaleKit :::" cd .. && make c-archive mkdir -p build xcodebuild test -scheme TailscaleKitXCTests \ diff --git a/swift/TailscaleKit.xcodeproj/project.pbxproj b/swift/TailscaleKit.xcodeproj/project.pbxproj index 6490ed3..417c01d 100644 --- a/swift/TailscaleKit.xcodeproj/project.pbxproj +++ b/swift/TailscaleKit.xcodeproj/project.pbxproj @@ -647,6 +647,13 @@ buildSettings = { ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; + CLANG_WARN_FRAMEWORK_INCLUDE_PRIVATE_FROM_PUBLIC = YES; + CLANG_WARN_IMPLICIT_FALLTHROUGH = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -656,6 +663,13 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/.."; + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/..", @@ -681,11 +695,13 @@ PRODUCT_MODULE_NAME = TailscaleKit; PRODUCT_NAME = TailscaleKit; PROVISIONING_PROFILE_SPECIFIER = ""; + RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = macosx; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; XROS_DEPLOYMENT_TARGET = 2.1; @@ -697,6 +713,13 @@ buildSettings = { ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; + CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; + CLANG_WARN_FRAMEWORK_INCLUDE_PRIVATE_FROM_PUBLIC = YES; + CLANG_WARN_IMPLICIT_FALLTHROUGH = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -706,6 +729,13 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/.."; + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_SHADOW = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/..", @@ -731,11 +761,13 @@ PRODUCT_MODULE_NAME = TailscaleKit; PRODUCT_NAME = TailscaleKit; PROVISIONING_PROFILE_SPECIFIER = ""; + RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = macosx; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2,7"; XROS_DEPLOYMENT_TARGET = 2.1; @@ -753,15 +785,20 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_FRAMEWORK_INCLUDE_PRIVATE_FROM_PUBLIC = YES; + CLANG_WARN_IMPLICIT_FALLTHROUGH = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; @@ -770,7 +807,9 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; @@ -789,16 +828,31 @@ "DEBUG=1", "$(inherited)", ); + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_PEDANTIC = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + RUN_CLANG_STATIC_ANALYZER = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VERSIONING_SYSTEM = "apple-generic"; @@ -817,15 +871,20 @@ CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_ASSIGN_ENUM = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; + CLANG_WARN_COMPLETION_HANDLER_MISUSE = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_FRAMEWORK_INCLUDE_PRIVATE_FROM_PUBLIC = YES; + CLANG_WARN_IMPLICIT_FALLTHROUGH = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES_ERROR; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; @@ -834,7 +893,9 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES_ERROR; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; @@ -847,15 +908,30 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; + GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_PEDANTIC = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNKNOWN_PRAGMAS = YES; GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + RUN_CLANG_STATIC_ANALYZER = YES; SWIFT_COMPILATION_MODE = wholemodule; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/swift/TailscaleKit/IncomingConnection.swift b/swift/TailscaleKit/IncomingConnection.swift index b8cacb8..f35f3e1 100644 --- a/swift/TailscaleKit/IncomingConnection.swift +++ b/swift/TailscaleKit/IncomingConnection.swift @@ -14,12 +14,19 @@ public actor IncomingConnection { public let remoteAddress: String? - @Published public var state: ConnectionState = .idle + @Published var _state: ConnectionState = .idle + + public func state() -> any AsyncSequence { + $_state + .removeDuplicates() + .eraseToAnyPublisher() + .values + } init(conn: TailscaleConnection, remoteAddress: String?, logger: LogSink? = nil) async { self.logger = logger self.conn = conn - self.state = .connected + _state = .connected self.remoteAddress = remoteAddress reader = SocketReader(conn: conn) } @@ -35,13 +42,13 @@ public actor IncomingConnection { unistd.close(conn) conn = 0 } - state = .closed + _state = .closed } /// Returns up to size bytes from the connection. Blocks until /// data is available public func receive(maximumLength: Int = 4096, timeout: Int32) async throws -> Data { - guard state == .connected else { + guard _state == .connected else { throw TailscaleError.connectionClosed } @@ -50,7 +57,7 @@ public actor IncomingConnection { /// Reads a complete message from the connection public func receiveMessage( timeout: Int32) async throws -> Data { - guard state == .connected else { + guard _state == .connected else { throw TailscaleError.connectionClosed } diff --git a/swift/TailscaleKit/Listener.swift b/swift/TailscaleKit/Listener.swift index 23b6737..0fdab8f 100644 --- a/swift/TailscaleKit/Listener.swift +++ b/swift/TailscaleKit/Listener.swift @@ -14,7 +14,14 @@ public actor Listener { private let logger: LogSink? - @Published public var state: ListenterState = .idle + @Published var _state: ListenerState = .idle + + public func state() -> any AsyncSequence { + $_state + .removeDuplicates() + .eraseToAnyPublisher() + .values + } /// Initializes and readies a new listener /// @@ -34,13 +41,13 @@ public actor Listener { let res = tailscale_listen(tailscale, proto.rawValue, address, &listener) guard res == 0 else { - state = .failed + _state = .failed let msg = tailscale.getErrorMessage() let err = TailscaleError.fromPosixErrCode(res, msg) logger?.log("Listener failed to initialize: \(msg) (\(err.localizedDescription))") throw err } - state = .listening + _state = .listening } deinit { @@ -56,7 +63,7 @@ public actor Listener { unistd.close(listener) listener = 0 } - state = .closed + _state = .closed } /// Blocks and awaits a new incoming connection diff --git a/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift b/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift index 01387ba..7133505 100644 --- a/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift +++ b/swift/TailscaleKit/LocalAPI/LocalAPIClient.swift @@ -5,49 +5,77 @@ import Foundation let kLocalAPIPath = "/localapi/v0/" +/// LocalAPIError enumerates the various errors that may be returned when making requests +/// to localAPI. public enum LocalAPIError: Error, LocalizedError { case localAPIBadResponse case localAPIStatusError(status: Int, body: String) case localAPIURLRequestError case localAPIBugReportError case localAPIJSONEncodeError - case notConnected - case noCredentials - case noSessionID } -enum HTTPMethod: String { - case GET - case POST - case PATCH - case PUT - case DELETE -} +public actor LocalAPIClient { + enum HTTPMethod: String { + case GET + case POST + case PATCH + case PUT + case DELETE + } -enum LocalAPIEndpoint: String { - case prefs = "prefs" - case start = "start" - case loginInteractive = "login-interactive" - case resetAuth = "reset-auth" - case logout = "logout" - case profiles = "profiles" - case profilesCurrent = "profiles/current" - case status = "status" - case watchIPNBus = "watch-ipn-bus" -} + enum LocalAPIEndpoint: String { + case prefs = "prefs" + case start = "start" + case loginInteractive = "login-interactive" + case resetAuth = "reset-auth" + case logout = "logout" + case profiles = "profiles" + case profilesCurrent = "profiles/current" + case status = "status" + case watchIPNBus = "watch-ipn-bus" + } -public actor LocalAPIClient { - /// The local node for proxying requests + /// The local node that will be handling our localAPI requests. let node: TailscaleNode let logger: LogSink? + public init(localNode: TailscaleNode, logger: LogSink?) { self.node = localNode self.logger = logger } + + // MARK: - IPN Bus + + /// watchIPNBus subscribes to the IPN notification bus. This is the primary mechanism that should be implemented for observing + /// changes to the state of the tailnet. This opens a long-polling HTTP request and feeds events to the consumer, via the processor. + /// For large tailnets, or tailnets with large numbers of ephemeral devices, it is recommended that you always pass the rateLimitNetmaps + /// option. Netap updates may be large and frequent and the resources required to parse the incoming JSON are non-trivial.. The + /// rateLimitNetmaps option will limit netmap updates to roughly one ever 3 seconds. + /// + /// You may spawn multiple bus watchers, but a single application-wide watcher with the full set of opts will often suffice. + /// + /// - Parameters: + /// - mask: a mask indicating the events we wish to observe + /// - consumer: an actor implementing MessageConsumer to which incoming events will be sent + /// - Returns: The MessageProcessor handling the incoming event stream. This should be destroyed/stopped when the caller + /// wishes to unsubscribe from the event stream. + public func watchIPNBus(mask: Ipn.NotifyWatchOpt, consumer: MessageConsumer) async throws -> MessageProcessor { + let params = [URLQueryItem(name: "mask", value: String(mask.rawValue))] + let (request, sessionConfig) = try await self.basicAuthURLRequest(endpoint: .watchIPNBus, + method: .GET, + params: params) + + let messageProcessor = await MessageProcessor(consumer: consumer, logger: logger) + messageProcessor.start(request, config: sessionConfig) + return messageProcessor + } + // MARK: - Prefs + /// getPrefs returns the Ipn.Prefs for the current user public func getPrefs() async throws -> Ipn.Prefs { let result = await doSimpleAPIRequest( endpoint: .prefs, @@ -63,12 +91,13 @@ public actor LocalAPIClient { } } + /// editPrefs submits a request to edit the current users prefs using the given mask @discardableResult - public func editPrefs(prefs: Ipn.MaskedPrefs) async throws -> Ipn.Prefs { + public func editPrefs(mask: Ipn.MaskedPrefs) async throws -> Ipn.Prefs { let result = await doJSONAPIRequest( endpoint: .prefs, method: .PATCH, - bodyAsJSON: prefs, + bodyAsJSON: mask, resultTransformer: jsonDecodeTransformer(Ipn.Prefs.self)) switch result { @@ -80,6 +109,13 @@ public actor LocalAPIClient { } } + // NOTE:- The Account Management and Profiles APIs are not yet fully supported + // via TailscaleKit. Use with caution. These may disappear in future + // versions but are included for those that wish to experiment with + // browser based auth and multi-user environments. + // + // See TailscaleNode for most of the equivalent functionality. + // MARK: - Account Management public func start(options: Ipn.Options) async throws { @@ -187,6 +223,9 @@ public actor LocalAPIClient { // MARK: - Status + /// backendStatus returns the current status of the backend. + /// + /// The majority of the information this returns can be observed using watchIPNBus. public func backendStatus() async throws -> IpnState.Status { let result = await doSimpleAPIRequest( endpoint: .status, @@ -199,33 +238,6 @@ public actor LocalAPIClient { } } - // MARK: - IPN Bus - - /// watchIPNBus subscribes to the IPN notification bus. - /// - Parameters: - /// - mask: options for what notifications - /// - notifyHandler: function invoked on the main thread with notify payloads as they are received - /// - errorHandler: function invoked on the main thread with errors as they happen - /// - Returns: function that can be called to stop the subscription - public func watchIPNBus(mask: Ipn.NotifyWatchOpt, consumer: MessageConsumer) async -> MessageProcessor? { - let params = [URLQueryItem(name: "mask", value: String(mask.rawValue))] - do { - let (request, sessionConfig) = try await self.basicAuthURLRequest(endpoint: .watchIPNBus, - method: .GET, - params: params) - - let messageProcessor = await MessageProcessor(consumer: consumer, logger: logger) - messageProcessor.start(request, config: sessionConfig) - - return messageProcessor - - } catch { - await consumer.error(LocalAPIError.localAPIURLRequestError) - return nil - } - - } - // MARK: - Requests private func basicAuthURLRequest(endpoint: LocalAPIEndpoint, @@ -361,14 +373,14 @@ public actor LocalAPIClient { // MARK: - Transformers - func errorTransformer(result: Result) -> Error? { + private func errorTransformer(result: Result) -> Error? { switch result { case .success: return nil case .failure(let error): return error } } - func jsonDecodeTransformer(_ type: T.Type) -> (_ result: Result) -> Result { + private func jsonDecodeTransformer(_ type: T.Type) -> (_ result: Result) -> Result { return { result in switch result { case .success(let data): diff --git a/swift/TailscaleKit/OutgoingConnection.swift b/swift/TailscaleKit/OutgoingConnection.swift index ed6e296..3ef4ae3 100644 --- a/swift/TailscaleKit/OutgoingConnection.swift +++ b/swift/TailscaleKit/OutgoingConnection.swift @@ -12,9 +12,8 @@ public enum ConnectionState { case failed ///< The attempt to dial the connection failed } - -/// ListnerState indicates the state of individual TSListener instances -public enum ListenterState { +/// ListenerState indicates the state of individual TSListener instances +public enum ListenerState { case idle ///< Waiting. case listening ///< Listening case closed ///< Closed and ready to be disposed of. diff --git a/swift/TailscaleKitXCTests/TailscaleKitTests.swift b/swift/TailscaleKitXCTests/TailscaleKitTests.swift index d17eeaa..8f6333e 100644 --- a/swift/TailscaleKitXCTests/TailscaleKitTests.swift +++ b/swift/TailscaleKitXCTests/TailscaleKitTests.swift @@ -13,7 +13,9 @@ final class TailscaleKitTests: XCTestCase { let res = buf.withUnsafeMutableBufferPointer { ptr in return run_control(ptr.baseAddress!, 1024) } - controlURL = String(validatingCString: buf) ?? "" + let len = buf.firstIndex(where: { $0 == 0 }) ?? 0 + let str = buf[0.. 0) + let peerStatus = status.SelfStatus! + XCTAssertTrue(peerStatus.Online) + } catch { + XCTFail(error.localizedDescription) } } }