diff --git a/punchnet/AbortView.swift b/punchnet/AbortView.swift new file mode 100644 index 0000000..7c05c29 --- /dev/null +++ b/punchnet/AbortView.swift @@ -0,0 +1,90 @@ +// +// AbortView.swift +// sdlan +// +// Created by 安礼成 on 2024/6/5. +// + +import Foundation +import SwiftUI + +struct AbortView: View { + + struct AlertShow: Identifiable { + enum ShowContent { + case error(String) + case upgrade(String, String) + } + + var id: String + var content: ShowContent + } + + @State private var alertShow: AlertShow? + + var body: some View { + VStack { + Image("logo") + + Text("sdlan") + + Text("Version1.1") + + Button { + Task { + guard let response = try? await SDLAPI.checkVersion(clientId: "test", version: 1, channel: "macos") else { + DispatchQueue.main.async { + self.alertShow = AlertShow(id: "network_error", content: .error("Network Error")) + } + return + } + + if let result = response.result { + if result.upgrade_type == 0 { + DispatchQueue.main.async { + self.alertShow = AlertShow(id: "upgrade_0", content: .upgrade(result.upgrade_prompt, "")) + } + } else if result.upgrade_type == 1 { + DispatchQueue.main.async { + self.alertShow = AlertShow(id: "upgrade_1", content: .upgrade(result.upgrade_prompt, result.upgrade_address)) + } + } else if result.upgrade_type == 2 { + DispatchQueue.main.async { + self.alertShow = AlertShow(id: "upgrade_1", content: .upgrade(result.upgrade_prompt, result.upgrade_address)) + } + } + } else if let error = response.error { + DispatchQueue.main.async { + self.alertShow = AlertShow(id: "response_error", content: .error(error.message)) + } + } + } + + } label: { + Text("版本检测") + .font(.system(size: 16, weight: .regular)) + .foregroundColor(.white) + .cornerRadius(5.0) + } + .frame(width: 138, height: 33) + .buttonStyle(PlainButtonStyle()) + .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) + .cornerRadius(5.0) + + } + .alert(item: $alertShow) { show in + switch show.content { + case .error(let errorMessage): + Alert(title: Text("错误提示"), message: Text(errorMessage)) + case .upgrade(let prompt, let address): + Alert(title: Text("版本升级"), message: Text(prompt), primaryButton: .default(Text("升级版本"), action: { + if let url = URL(string: address) { + // schema: "macappstore://apps.apple.com/app/idYOUR_APP_ID" + NSWorkspace.shared.open(url) + } + }), secondaryButton: .cancel()) + } + } + } + +} diff --git a/punchnet/Assets.xcassets/logo.imageset/Contents.json b/punchnet/Assets.xcassets/logo.imageset/Contents.json new file mode 100644 index 0000000..769bacf --- /dev/null +++ b/punchnet/Assets.xcassets/logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logo.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/punchnet/Assets.xcassets/logo.imageset/logo.jpg b/punchnet/Assets.xcassets/logo.imageset/logo.jpg new file mode 100644 index 0000000..ce597f5 Binary files /dev/null and b/punchnet/Assets.xcassets/logo.imageset/logo.jpg differ diff --git a/punchnet/ContentView.swift b/punchnet/ContentView.swift index f21d3af..3b29c91 100644 --- a/punchnet/ContentView.swift +++ b/punchnet/ContentView.swift @@ -1,18 +1,101 @@ // // ContentView.swift -// punchnet +// sdlan // -// Created by 安礼成 on 2025/5/12. +// Created by 安礼成 on 2024/1/17. // import SwiftUI import SwiftData +import Combine struct ContentView: View { - @Environment(\.modelContext) private var modelContext - @Query private var items: [Item] + //@Environment(\.modelContext) private var modelContext + //@Query private var items: [Item] + + @AppStorage("token") private var token: String = "" + @ObservedObject private var vpnManager = VPNManager.shared + @State private var showAlert = false + @State private var showStunAlert = false + @State private var message: NoticeMessage.InboundMessage = .none + @State private var cancel: AnyCancellable? var body: some View { + + VStack(alignment: .center, spacing: 10) { + + Image("logo") + + Text("PUNCHENT") + .font(.system(size: 46, weight: .bold)) + .foregroundColor(.white) + .cornerRadius(5.0) + + Spacer() + .frame(width: 1, height: 10) + + TextField("邀请码", text: $token) + .multilineTextAlignment(.center) + .frame(width: 245, height: 27) + .cornerRadius(5.0) + + Spacer() + .frame(width: 1, height: 10) + + Button(action: { + Task { + switch self.vpnManager.vpnStatus { + case .connected: + try await vpnManager.disableVpn() + case .disconnected: + if self.token.isEmpty { + self.showAlert = true + return + } + try await vpnManager.enableVpn(options: [ + "version:": SystemConfig.version as NSObject, + "installed_channel": SystemConfig.installedChannel as NSObject, + "token": self.token as NSObject + ]) + } + } + }, label: { + Text(vpnManager.title) + .font(.system(size: 16, weight: .regular)) + .foregroundColor(vpnManager.color) + .cornerRadius(5.0) + }) + .frame(width: 138, height: 33) + .buttonStyle(PlainButtonStyle()) + .background(Color(red: 74 / 255, green: 207 / 255, blue: 154 / 255)) + .cornerRadius(5.0) + } + .frame(width: 380, height: 560) + .background(Color(red: 36 / 255, green: 38 / 255, blue: 51 / 255)) + .alert(isPresented: $showAlert) { + Alert(title: Text("请输入正确的邀请码")) + } + .alert(isPresented: $showStunAlert) { + switch self.message { + case .upgradeMessage(let upgradeMessage): + Alert(title: Text(upgradeMessage.prompt)) + case .alertMessage(let alertMessage): + Alert(title: Text(alertMessage.alert)) + default: + Alert(title: Text("")) + } + } + .onAppear { + self.cancel = UDPNoticeCenterServer.shared.messageFlow.sink{ message in + DispatchQueue.main.async { + self.message = message + self.showStunAlert = true + } + } + } + + + /* NavigationSplitView { List { ForEach(items) { item in @@ -35,25 +118,26 @@ struct ContentView: View { } detail: { Text("Select an item") } + */ } + private func addItem() { withAnimation { - let newItem = Item(timestamp: Date()) - modelContext.insert(newItem) + } } private func deleteItems(offsets: IndexSet) { withAnimation { - for index in offsets { - modelContext.delete(items[index]) - } +// for index in offsets { +// modelContext.delete(items[index]) +// } } } } #Preview { ContentView() - .modelContainer(for: Item.self, inMemory: true) + //.modelContainer(for: Item.self, inMemory: true) } diff --git a/punchnet/Core/NoticeMessage.swift b/punchnet/Core/NoticeMessage.swift new file mode 100644 index 0000000..297d7b5 --- /dev/null +++ b/punchnet/Core/NoticeMessage.swift @@ -0,0 +1,50 @@ +// +// NoticeMessage.swift +// sdlan +// +// Created by 安礼成 on 2024/6/3. +// + +import Foundation + +struct NoticeMessage { + // 消息类型 + enum NoticeType: UInt8 { + case upgrade = 1 + case alert = 2 + } + + struct UpgradeMessage: Codable { + let prompt: String + let address: String + + var binaryData: Data { + let json = try! JSONEncoder().encode(self) + var data = Data() + data.append(contentsOf: [NoticeType.upgrade.rawValue]) + data.append(json) + + return data + } + } + + struct AlertMessage: Codable { + let alert: String + + var binaryData: Data { + let json = try! JSONEncoder().encode(self) + var data = Data() + data.append(contentsOf: [NoticeType.alert.rawValue]) + data.append(json) + + return data + } + } + + enum InboundMessage { + case none + case upgradeMessage(UpgradeMessage) + case alertMessage(AlertMessage) + } + +} diff --git a/punchnet/Core/SDLAPI.swift b/punchnet/Core/SDLAPI.swift new file mode 100644 index 0000000..e3a5824 --- /dev/null +++ b/punchnet/Core/SDLAPI.swift @@ -0,0 +1,47 @@ +// +// SDLApi.swift +// sdlan +// +// Created by 安礼成 on 2024/6/5. +// + +import Foundation + +struct JSONRPCResponse: Decodable { + let result: T? + let error: JSONRPCError? +} + +struct JSONRPCError: Decodable { + let code: Int + let message: String + let data: String? +} + +struct SDLAPI { + + struct Upgrade: Decodable { + let upgrade_type: Int + let upgrade_prompt: String + let upgrade_address: String + } + + static func checkVersion(clientId: String, version: Int, channel: String) async throws -> JSONRPCResponse { + let params: [String:Any] = [ + "client_id": clientId, + "version": version, + "channel": channel + ] + + let postData = try! JSONSerialization.data(withJSONObject: params) + var request = URLRequest(url: URL(string: "http://127.0.0.1:18082/test/upgrade")!) + request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = postData + + let (data, _) = try await URLSession.shared.data(for: request) + + return try JSONDecoder().decode(JSONRPCResponse.self, from: data) + } + +} diff --git a/punchnet/Core/SystemConfig.swift b/punchnet/Core/SystemConfig.swift new file mode 100644 index 0000000..9f00db5 --- /dev/null +++ b/punchnet/Core/SystemConfig.swift @@ -0,0 +1,18 @@ +// +// SystemConfig.swift +// sdlan +// +// Created by 安礼成 on 2024/6/3. +// + +import Foundation + +struct SystemConfig { + // 版本设置 + static let version = 1 + + static let version_name = "1.1" + + // 安装渠道 + static let installedChannel = "MacAppStore" +} diff --git a/punchnet/Core/UDPNoticeCenterServer.swift b/punchnet/Core/UDPNoticeCenterServer.swift new file mode 100644 index 0000000..e0f3bf0 --- /dev/null +++ b/punchnet/Core/UDPNoticeCenterServer.swift @@ -0,0 +1,86 @@ +// +// UDPMessageCenterServer.swift +// sdlan +// +// Created by 安礼成 on 2024/5/20. +// + +import Foundation +import NIOCore +import NIOPosix +import Combine + +final class UDPNoticeCenterServer: ChannelInboundHandler { + public typealias InboundIn = AddressedEnvelope + public typealias OutboundOut = AddressedEnvelope + + private var group: MultiThreadedEventLoopGroup? + private var thread: Thread? + + var messageFlow = PassthroughSubject() + static let shared = UDPNoticeCenterServer() + + private init() { + + } + + func start() { + self.thread = Thread { + self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let bootstrap = DatagramBootstrap(group: self.group!) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelInitializer { channel in + channel.pipeline.addHandler(self) + } + + let channel = try! bootstrap.bind(host: "127.0.0.1", port: 50195).wait() + try! channel.closeFuture.wait() + } + self.thread?.start() + } + + func stop() { + self.thread?.cancel() + try? self.group?.syncShutdownGracefully() + } + + // --MARK: ChannelInboundHandler + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let envelope = self.unwrapInboundIn(data) + var buffer = envelope.data + guard let type = buffer.readInteger(as: UInt8.self), + let noticeType = NoticeMessage.NoticeType(rawValue: type), + let bytes = buffer.readBytes(length: buffer.readableBytes) else { + return + } + + switch noticeType { + case .upgrade: + if let upgradeMessage = try? JSONDecoder().decode(NoticeMessage.UpgradeMessage.self, from: Data(bytes)) { + DispatchQueue.main.async { + self.messageFlow.send(.upgradeMessage(upgradeMessage)) + } + } + case .alert: + if let alertMessage = try? JSONDecoder().decode(NoticeMessage.AlertMessage.self, from: Data(bytes)) { + DispatchQueue.main.async { + self.messageFlow.send(.alertMessage(alertMessage)) + } + } + } + } + + public func channelReadComplete(context: ChannelHandlerContext) { + // As we are not really interested getting notified on success or failure we just pass nil as promise to + // reduce allocations. + context.flush() + } + + public func errorCaught(context: ChannelHandlerContext, error: Error) { + // As we are not really interested getting notified on success or failure we just pass nil as promise to + // reduce allocations. + context.close(promise: nil) + } + +} diff --git a/punchnet/Extension/DataExtension.swift b/punchnet/Extension/DataExtension.swift new file mode 100644 index 0000000..f5fafcc --- /dev/null +++ b/punchnet/Extension/DataExtension.swift @@ -0,0 +1,35 @@ +// +// DataExtension.swift +// sdlan +// +// Created by 安礼成 on 2024/2/1. +// + +import Foundation + +extension Data { + + mutating public func append(int32val: Int32) { + self.append(contentsOf: [ + (UInt8) (int32val >> 24 & 0xFF), + (UInt8) (int32val >> 16 & 0xFF), + (UInt8) (int32val >> 8 & 0xFF), + (UInt8) (int32val & 0xFF) + ]) + } + + mutating public func append(int16val: Int16) { + self.append(contentsOf: [ + (UInt8) (int16val >> 8 & 0xFF), + (UInt8) (int16val & 0xFF) + ]) + } + + mutating public func append(str: String) { + if let data = str.data(using: .utf8) { + self.append(contentsOf: data) + } + } + +} + diff --git a/punchnet/VPNManager.swift b/punchnet/VPNManager.swift new file mode 100644 index 0000000..0108cf3 --- /dev/null +++ b/punchnet/VPNManager.swift @@ -0,0 +1,98 @@ +// +// VPNManager.swift +// sdlan +// +// Created by 安礼成 on 2024/1/17. +// + +import Foundation +import NetworkExtension +import SwiftUI + +// vpn管理类 +class VPNManager: ObservableObject { + static let shared = VPNManager() + + @Published var vpnStatus: VPNStatus = .disconnected + @Published var title: String = "启动" + @Published var color: Color = .white + + enum VPNStatus { + case connected + case disconnected + } + + private init() { + + } + + // 开启vpn + func enableVpn(options: [String : NSObject]? = nil) async throws { + let manager = try await loadAndCreateProviderManager() + try await manager.loadFromPreferences() + self.addVPNStatusObserver(manager) + + try manager.connection.startVPNTunnel(options: options) + } + + // 关闭vpn + func disableVpn() async throws { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + managers.first?.connection.stopVPNTunnel() + } + + // MARK: - Private Methods + + // 监控系统VPN的状态的变化 + private func addVPNStatusObserver(_ manager: NETunnelProviderManager) { + NotificationCenter.default.removeObserver(self) + + NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: manager.connection, queue: .main) { [unowned self] (notification) -> Void in + // 更新vpn的状态 + switch manager.connection.status { + case .invalid, .disconnected, .disconnecting: + self.vpnStatus = .disconnected + self.title = "启动" + self.color = .white + case .connecting, .connected, .reasserting: + self.vpnStatus = .connected + self.title = "停止" + self.color = .red + @unknown default: + self.vpnStatus = .disconnected + self.title = "启动" + self.color = .red + } + } + } + + // MARK: - Private Methods + + // 加载或者创建相关的配置 + private func loadAndCreateProviderManager() async throws -> NETunnelProviderManager { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + + let manager = managers.first ?? NETunnelProviderManager() + manager.localizedDescription = "sdlan" + manager.isEnabled = true + + // 设置相关参数,在PacketTunnel中可以用 + let protocolConfiguration = NETunnelProviderProtocol() + protocolConfiguration.serverAddress = "sdlan" + protocolConfiguration.providerConfiguration = [String:AnyObject]() + protocolConfiguration.providerBundleIdentifier = "com.jihe.sdlan.Tun" + manager.protocolConfiguration = protocolConfiguration + manager.isOnDemandEnabled = false + + manager.isEnabled = true + + try await manager.saveToPreferences() + + return manager + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + +} diff --git a/punchnet/sdlan.entitlements b/punchnet/sdlan.entitlements new file mode 100644 index 0000000..3e702e4 --- /dev/null +++ b/punchnet/sdlan.entitlements @@ -0,0 +1,28 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + com.apple.developer.networking.vpn.api + + allow-vpn + + com.apple.developer.system-extension.install + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(TeamIdentifierPrefix) + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/punchnet/sdlanApp.swift b/punchnet/sdlanApp.swift new file mode 100644 index 0000000..66902da --- /dev/null +++ b/punchnet/sdlanApp.swift @@ -0,0 +1,117 @@ +// +// sdlanApp.swift +// sdlan +// +// Created by 安礼成 on 2024/1/17. +// + +import SwiftUI +import AppKit +import SwiftData +import Combine + +@main +struct sdlanApp: App { + /* + var sharedModelContainer: ModelContainer = { + let schema = Schema([ + Item.self, + ]) + let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + do { + return try ModelContainer(for: schema, configurations: [modelConfiguration]) + } catch { + fatalError("Could not create ModelContainer: \(error)") + } + }() + */ + + @Environment(\.openWindow) private var openWindow + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + @AppStorage("token") var token: String = "" + @ObservedObject var vpnManager = VPNManager.shared + + var body: some Scene { + WindowGroup(id: "mainWindow") { + ContentView() + } + .commands { + CommandGroup(replacing: .appInfo) { + Button { + openWindow(id: "abortSDLAN") + } label: { + Text("About sdlan") + } + } + } + + Window("", id: "abortSDLAN") { + AbortView() + } + + //.modelContainer(sharedModelContainer) + MenuBarExtra("sdlanApp", systemImage: "hammer") { + VStack { + Button(action: { + self.menuClick() + }, label: { + Text(vpnManager.title) + }) + + Divider() + + Button(action: { + NSApplication.shared.terminate(nil) + }, label: { Text("退出") }) + + } + } + .menuBarExtraStyle(.menu) + } + + private func menuClick() { + switch self.vpnManager.vpnStatus { + case .disconnected: + if token.isEmpty { + let windows = NSApplication.shared.windows + if let window = windows.first(where: {$0.title == "sdlan"}) { + window.level = .floating + } else { + self.openWindow(id: "mainWindow") + } + } else { + Task { + try await vpnManager.enableVpn(options: ["token": token as NSObject]) + } + } + case .connected: + Task { + try await vpnManager.disableVpn() + } + } + } + +} + +// 处理APP的生命周期 +class AppDelegate: NSObject, NSApplicationDelegate { + + func applicationWillFinishLaunching(_ notification: Notification) { + UDPNoticeCenterServer.shared.start() + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + Task { + try await VPNManager.shared.disableVpn() + DispatchQueue.main.async { + sender.reply(toApplicationShouldTerminate: true) + } + UDPNoticeCenterServer.shared.stop() + } + + return .terminateLater + } + +}