Swift SDK: Swift Package wrapping libquicproquo_ffi with QpqClient class (connect, login, send, receive, disconnect) for iOS 15+ / macOS 13+. Kotlin SDK: JNI bridge to libquicproquo_ffi with QpqClient class for Android (aarch64, armv7) and JVM, Gradle build configuration. Adds RegisterPushToken RPC (method ID 710) to device.proto for APNs/FCM/WebPush device push token registration.
156 lines
4.6 KiB
Swift
156 lines
4.6 KiB
Swift
import Foundation
|
|
import CQuicProQuo
|
|
|
|
/// High-level quicproquo client for iOS/macOS.
|
|
///
|
|
/// Wraps ``libquicproquo_ffi`` to provide a Swift-native API for
|
|
/// connecting, authenticating, and messaging.
|
|
///
|
|
/// ```swift
|
|
/// let client = try QpqClient(
|
|
/// server: "127.0.0.1:5001",
|
|
/// caCertPath: "/path/to/ca.pem"
|
|
/// )
|
|
/// try client.login(username: "alice", password: "secret")
|
|
/// try client.send(to: "bob", message: "hello".data(using: .utf8)!)
|
|
/// let messages = try client.receive(timeoutMs: 5000)
|
|
/// client.disconnect()
|
|
/// ```
|
|
public final class QpqClient: @unchecked Sendable {
|
|
private var handle: OpaquePointer?
|
|
private let lock = NSLock()
|
|
|
|
/// Connect to a quicproquo server.
|
|
///
|
|
/// - Parameters:
|
|
/// - server: Server address as ``host:port``.
|
|
/// - caCertPath: Path to the PEM-encoded CA certificate.
|
|
/// - serverName: TLS SNI server name (defaults to host from *server*).
|
|
/// - Throws: ``QpqError/connectionFailed(_:)`` if the connection fails.
|
|
public init(server: String, caCertPath: String, serverName: String? = nil) throws {
|
|
let sn = serverName ?? server.components(separatedBy: ":").first ?? server
|
|
|
|
let h = qpq_connect(server, caCertPath, sn)
|
|
guard let h else {
|
|
throw QpqError.connectionFailed("qpq_connect returned NULL for \(server)")
|
|
}
|
|
self.handle = h
|
|
}
|
|
|
|
deinit {
|
|
disconnect()
|
|
}
|
|
|
|
// MARK: - Authentication
|
|
|
|
/// Authenticate with the server using OPAQUE (username + password).
|
|
///
|
|
/// - Throws: ``QpqError/authFailed(_:)`` on bad credentials.
|
|
public func login(username: String, password: String) throws {
|
|
try withHandle { h in
|
|
let code = qpq_login(h, username, password)
|
|
try checkStatus(code, handle: h)
|
|
}
|
|
}
|
|
|
|
// MARK: - Messaging
|
|
|
|
/// Send a message to a recipient by username.
|
|
///
|
|
/// The message is encrypted via MLS before delivery.
|
|
///
|
|
/// - Parameters:
|
|
/// - recipient: Recipient username.
|
|
/// - message: Message payload (arbitrary bytes).
|
|
public func send(to recipient: String, message: Data) throws {
|
|
try withHandle { h in
|
|
let code = message.withUnsafeBytes { buffer -> Int32 in
|
|
guard let ptr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
|
return QPQ_ERROR
|
|
}
|
|
return qpq_send(h, recipient, ptr, buffer.count)
|
|
}
|
|
try checkStatus(code, handle: h)
|
|
}
|
|
}
|
|
|
|
/// Receive pending messages, blocking up to ``timeoutMs`` milliseconds.
|
|
///
|
|
/// - Returns: An array of message strings (UTF-8).
|
|
public func receive(timeoutMs: UInt32 = 5000) throws -> [String] {
|
|
try withHandle { h in
|
|
var jsonPtr: UnsafeMutablePointer<CChar>?
|
|
let code = qpq_receive(h, timeoutMs, &jsonPtr)
|
|
try checkStatus(code, handle: h)
|
|
|
|
guard let jsonPtr else {
|
|
return []
|
|
}
|
|
defer { qpq_free_string(jsonPtr) }
|
|
|
|
let jsonString = String(cString: jsonPtr)
|
|
|
|
guard let data = jsonString.data(using: .utf8),
|
|
let array = try? JSONSerialization.jsonObject(with: data) as? [String]
|
|
else {
|
|
return []
|
|
}
|
|
return array
|
|
}
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/// Disconnect from the server and release resources.
|
|
public func disconnect() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
if let h = handle {
|
|
qpq_disconnect(h)
|
|
handle = nil
|
|
}
|
|
}
|
|
|
|
/// Whether the client is currently connected.
|
|
public var isConnected: Bool {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return handle != nil
|
|
}
|
|
|
|
// MARK: - Internal
|
|
|
|
private func withHandle<T>(_ body: (OpaquePointer) throws -> T) throws -> T {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
guard let h = handle else {
|
|
throw QpqError.notConnected
|
|
}
|
|
return try body(h)
|
|
}
|
|
|
|
private func checkStatus(_ code: Int32, handle: OpaquePointer) throws {
|
|
guard code != QPQ_OK else { return }
|
|
|
|
let msg: String
|
|
if let errPtr = qpq_last_error(handle) {
|
|
msg = String(cString: errPtr)
|
|
} else {
|
|
msg = "unknown error"
|
|
}
|
|
|
|
switch code {
|
|
case QPQ_AUTH_FAILED:
|
|
throw QpqError.authFailed(msg)
|
|
case QPQ_TIMEOUT:
|
|
throw QpqError.timeout(msg)
|
|
case QPQ_NOT_CONNECTED:
|
|
throw QpqError.notConnected
|
|
default:
|
|
throw QpqError.ffiError(msg)
|
|
}
|
|
}
|
|
}
|