Add support for tvOS

This commit is contained in:
世界
2023-07-29 09:44:31 +08:00
parent 5a8f3a770f
commit bb0385f53c
63 changed files with 1542 additions and 389 deletions
+3 -3
View File
@@ -1,5 +1,6 @@
import Foundation
import GRDB
import Network
public class Profile: Record, Identifiable, ObservableObject {
public var id: Int64?
@@ -15,16 +16,15 @@ public class Profile: Record, Identifiable, ObservableObject {
@Published public var autoUpdate: Bool
public var lastUpdated: Date?
public init(id: Int64? = nil, name: String, order: UInt32 = 0, type: ProfileType, path: String, remoteURL: String? = nil, lastUpdated: Date? = nil) {
public init(id: Int64? = nil, name: String, order: UInt32 = 0, type: ProfileType, path: String, remoteURL: String? = nil, autoUpdate: Bool = false, lastUpdated: Date? = nil) {
self.id = id
self.name = name
self.order = order
self.type = type
self.path = path
self.remoteURL = remoteURL
self.autoUpdate = autoUpdate
self.lastUpdated = lastUpdated
autoUpdate = false
super.init()
}
+63
View File
@@ -0,0 +1,63 @@
import Foundation
import Libbox
import Network
public class NWSocket {
private let connection: NWConnection
public init(_ connection: NWConnection) {
self.connection = connection
}
public func read() throws -> Data {
let semaphore = DispatchSemaphore(value: 0)
var result: Result<Data, Error>!
connection.receive(minimumIncompleteLength: 2, maximumLength: 2) { content, _, _, error in
if let error {
result = .failure(error)
} else {
result = .success(content!)
}
semaphore.signal()
}
semaphore.wait()
let lengthChunk = try result.get()
let length = Int(LibboxDecodeLengthChunk(lengthChunk))
connection.receive(minimumIncompleteLength: length, maximumLength: length) { content, _, _, error in
if let error {
result = .failure(error)
} else {
result = .success(content!)
}
semaphore.signal()
}
semaphore.wait()
return try result.get()
}
public func write(_ data: Data?) throws {
guard let data else {
return
}
let semaphore = DispatchSemaphore(value: 0)
var result: Error?
connection.send(content: LibboxEncodeChunkedMessage(data), isComplete: false, completion: .contentProcessed { error in
result = error
semaphore.wait()
})
if let result {
throw result
}
}
public func send(_ data: Data?) {
guard let data else {
return
}
connection.send(content: LibboxEncodeChunkedMessage(data), completion: .idempotent)
}
public func cancel() {
connection.cancel()
}
}
+128
View File
@@ -0,0 +1,128 @@
import Foundation
import Libbox
import Network
public class ProfileServer {
private var listener: NWListener
@available(iOS 16.0, macOS 13.0, *)
public init() throws {
listener = try NWListener(using: .applicationService)
listener.service = NWListener.Service(applicationService: "sing-box:profile")
listener.newConnectionHandler = { connection in
connection.stateUpdateHandler = { state in
if state == .ready {
Task.detached {
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100)
ProfileConnection(connection).process()
}
}
}
connection.start(queue: .global())
}
}
public func start() {
listener.start(queue: .global())
}
public func cancel() {
listener.cancel()
}
class ProfileConnection {
private let connection: NWSocket
init(_ connection: NWConnection) {
self.connection = NWSocket(connection)
}
func process() {
do {
try writeProfilePreviewList()
} catch {
NSLog("profile server: write profile list: \(error.localizedDescription)")
writeError(error.localizedDescription)
return
}
do {
while true {
let message = try connection.read()
try processMessage(message)
}
} catch {
NSLog("profile server: process connection: \(error.localizedDescription)")
writeError(error.localizedDescription)
}
}
private func processMessage(_ data: Data) throws {
if data.count == 0 {
return
}
let messageType = Int64(data[0])
switch messageType {
case LibboxMessageTypeProfileContentRequest:
var error: NSError?
let request = LibboxDecodeProfileContentRequest(data, &error)
if let error {
throw error
}
let profile = try ProfileManager.get(request!.profileID)
guard let profile else {
throw NSError(domain: "profile not found", code: 0)
}
let content = LibboxProfileContent()
content.name = profile.name
switch profile.type {
case .local:
content.type = LibboxProfileTypeLocal
case .icloud:
content.type = LibboxProfileTypeiCloud
case .remote:
content.type = LibboxProfileTypeRemote
}
content.config = try profile.read()
if profile.type != .local {
content.remotePath = profile.remoteURL!
}
if profile.type == .remote {
content.autoUpdate = profile.autoUpdate
if let lastUpdated = profile.lastUpdated {
content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970)
}
}
try connection.write(content.encode())
default:
throw NSError(domain: "unexpected message type \(messageType)", code: 0)
}
}
private func writeProfilePreviewList() throws {
let profiles = try ProfileManager.list()
let encoder = LibboxProfileEncoder()
for profile in profiles {
let preview = LibboxProfilePreview()
preview.profileID = profile.mustID
preview.name = profile.name
switch profile.type {
case .local:
preview.type = LibboxProfileTypeLocal
case .icloud:
preview.type = LibboxProfileTypeiCloud
case .remote:
preview.type = LibboxProfileTypeRemote
}
encoder.append(preview)
}
try connection.write(encoder.encode())
}
private func writeError(_ message: String) {
let errorMessage = LibboxErrorMessage()
errorMessage.message = message
try? connection.write(errorMessage.encode())
}
}
}
+16 -14
View File
@@ -3,6 +3,8 @@ import Libbox
import NetworkExtension
open class ExtensionProvider: NEPacketTunnelProvider {
public static let errorFile = FilePath.workingDirectory.appendingPathComponent("network_extension_error")
public var username: String? = nil
private var commandServer: LibboxCommandServer!
private var boxService: LibboxBoxService!
@@ -10,6 +12,8 @@ open class ExtensionProvider: NEPacketTunnelProvider {
override open func startTunnel(options _: [String: NSObject]?) async throws {
NSLog("Here I am")
try? FileManager.default.removeItem(at: ExtensionProvider.errorFile)
do {
try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true)
} catch {
@@ -19,13 +23,17 @@ open class ExtensionProvider: NEPacketTunnelProvider {
if let username {
var error: NSError?
LibboxSetupWithUsername(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, username, &error)
LibboxSetupWithUsername(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, username, &error)
if let error {
writeFatalError("(packet-tunnel) error: setup service: \(error.localizedDescription)")
return
}
} else {
LibboxSetup(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath)
var isTVOS = false
#if os(tvOS)
isTVOS = true
#endif
LibboxSetup(FilePath.sharedDirectory.relativePath, FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, isTVOS)
}
var error: NSError?
@@ -36,7 +44,7 @@ open class ExtensionProvider: NEPacketTunnelProvider {
LibboxSetMemoryLimit(!SharedPreferences.disableMemoryLimit)
commandServer = LibboxNewCommandServer(FilePath.sharedDirectory.relativePath, serverInterface(self), Int32(SharedPreferences.maxLogLines))
commandServer = LibboxNewCommandServer(serverInterface(self), Int32(SharedPreferences.maxLogLines))
do {
try commandServer.start()
} catch {
@@ -58,19 +66,13 @@ open class ExtensionProvider: NEPacketTunnelProvider {
private func writeError(_ message: String) {
writeMessage(message)
#if os(iOS)
ServiceNotification.postServiceNotification(title: "Service Error", message: message)
#else
if Variant.useSystemExtension {
NSLog(message)
} else {
displayMessage(message) { _ in
}
}
#endif
try? message.write(to: ExtensionProvider.errorFile, atomically: true, encoding: .utf8)
}
public func writeFatalError(_ message: String) {
#if DEBUG
NSLog(message)
#endif
writeError(message)
cancelTunnelWithError(NSError(domain: message, code: 0))
}
@@ -91,7 +93,7 @@ open class ExtensionProvider: NEPacketTunnelProvider {
do {
configContent = try profile.read()
} catch {
writeFatalError("(packet-tunnel) error: read config file: \(error.localizedDescription)")
writeFatalError("(packet-tunnel) error: read config file \(profile.path): \(error.localizedDescription)")
return
}
var error: NSError?
+31 -14
View File
@@ -7,25 +7,42 @@ public enum FilePath {
public extension FilePath {
static let groupName = "group.\(packageName)"
static var sharedDirectory = defaultSharedDirectory
private static let defaultSharedDirectory: URL! = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: FilePath.groupName)
private static var defaultSharedDirectory: URL {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: FilePath.groupName)!
}
static var cacheDirectory: URL {
sharedDirectory
#if os(iOS)
static let sharedDirectory = defaultSharedDirectory!
#elseif os(tvOS)
static let sharedDirectory = defaultSharedDirectory
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true)
}
#elseif os(macOS)
static var sharedDirectory: URL! = defaultSharedDirectory
#endif
static var workingDirectory: URL {
cacheDirectory.appendingPathComponent("Working", isDirectory: true)
}
#if os(iOS)
static let cacheDirectory = sharedDirectory
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true)
#elseif os(tvOS)
static let cacheDirectory = sharedDirectory
#elseif os(macOS)
static var cacheDirectory: URL {
sharedDirectory
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Caches", isDirectory: true)
}
#endif
static var iCloudDirectory: URL {
FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true)
}
#if os(macOS)
static var workingDirectory: URL {
cacheDirectory.appendingPathComponent("Working", isDirectory: true)
}
#else
static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true)
#endif
static var iCloudDirectory: URL! = FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true)
}
public extension URL {
-45
View File
@@ -1,45 +0,0 @@
import Foundation
import UserNotifications
public enum ServiceNotification {
private static let delegate = Delegate()
public static func register() {
UNUserNotificationCenter.current().delegate = delegate
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) {
_, _ in
}
}
private static var listener: ((UNNotificationContent) -> Void)?
public static func setServiceNotificationListener(listener: @escaping (UNNotificationContent) -> Void) {
ServiceNotification.listener = listener
}
public static func removeServiceNotificationListener() {
ServiceNotification.listener = nil
}
public static func postServiceNotification(content: UNNotificationContent) {
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: "service-notification", content: content, trigger: nil))
}
public static func postServiceNotification(title: String, message: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = message
postServiceNotification(content: content)
}
private class Delegate: NSObject, UNUserNotificationCenterDelegate {
func userNotificationCenter(_: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
if let listener = ServiceNotification.listener {
listener(notification.request.content)
return []
} else {
return [.alert]
}
}
}
}
+2
View File
@@ -11,5 +11,7 @@ public enum Variant {
public static let applicationName = "SFI"
#elseif os(macOS)
public static let applicationName = "SFM"
#elseif os(tvOS)
public static let applicationName = "SFT"
#endif
}