From 4adbbccc63a36c5ecb7656189b69a7527f7919f8 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Mon, 5 May 2025 11:56:51 -0400 Subject: [PATCH] swift: add public methods for published states and update docs updates tailscale/tailscale#15802 The connection states weren't public. These are now exposed as AsyncSequences so that there's no public dependency on Combine. Documentation cleanup up and clarified slightly. Some minor reorganization of localAPI. Adds a test to ensure that localAPI is functional. Add the xcode equivalent of -wall -werror --- .../HelloFromTailscale/HelloManager.swift | 29 ++--- .../HelloFromTailscale/TailnetSettings.swift | 2 +- swift/Makefile | 9 ++ swift/TailscaleKit.xcodeproj/project.pbxproj | 76 +++++++++++ swift/TailscaleKit/IncomingConnection.swift | 17 ++- swift/TailscaleKit/Listener.swift | 15 ++- .../LocalAPI/LocalAPIClient.swift | 120 ++++++++++-------- swift/TailscaleKit/OutgoingConnection.swift | 5 +- .../TailscaleKitTests.swift | 40 +++--- 9 files changed, 209 insertions(+), 104 deletions(-) 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) } } }