mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
611 lines
19 KiB
Swift
611 lines
19 KiB
Swift
//
|
||
// Util.swift
|
||
// V2rayU
|
||
//
|
||
// Created by yanue on 2018/10/12.
|
||
// Copyright © 2018 yanue. All rights reserved.
|
||
//
|
||
|
||
import Cocoa
|
||
|
||
extension UserDefaults {
|
||
enum KEY: String {
|
||
// v2ray-core version
|
||
case xRayCoreVersion
|
||
// v2ray server item list
|
||
case v2rayServerList
|
||
// v2ray subscribe item list
|
||
case v2raySubList
|
||
// current v2ray server name
|
||
case v2rayCurrentServerName
|
||
// v2ray-core turn on status
|
||
case v2rayTurnOn
|
||
// v2ray-core log level
|
||
case v2rayLogLevel
|
||
// v2ray dns json txt
|
||
case v2rayDnsJson
|
||
|
||
// auth check version
|
||
case autoCheckVersion
|
||
// auto launch after login
|
||
case autoLaunch
|
||
// auto clear logs
|
||
case autoClearLog
|
||
// auto update servers
|
||
case autoUpdateServers
|
||
// auto select Fastest server
|
||
case autoSelectFastestServer
|
||
// pac|manual|global
|
||
case runMode
|
||
// gfw pac list url
|
||
case gfwPacListUrl
|
||
|
||
// base settings
|
||
// http host
|
||
case localHttpHost
|
||
// http port
|
||
case localHttpPort
|
||
// sock host
|
||
case localSockHost
|
||
// sock port
|
||
case localSockPort
|
||
// dns servers
|
||
case dnsServers
|
||
// enable udp
|
||
case enableUdp
|
||
// enable mux
|
||
case enableMux
|
||
// enable Sniffing
|
||
case enableSniffing
|
||
// mux Concurrent
|
||
case muxConcurrent
|
||
// pacPort
|
||
case localPacPort
|
||
|
||
// custom routing list
|
||
case routingCustomList
|
||
// routing selected rule
|
||
case routingSelectedRule
|
||
}
|
||
|
||
static func setBool(forKey key: KEY, value: Bool) {
|
||
UserDefaults.standard.set(value, forKey: key.rawValue)
|
||
}
|
||
|
||
static func getBool(forKey key: KEY) -> Bool {
|
||
return UserDefaults.standard.bool(forKey: key.rawValue)
|
||
}
|
||
|
||
static func set(forKey key: KEY, value: String) {
|
||
UserDefaults.standard.set(value, forKey: key.rawValue)
|
||
}
|
||
|
||
static func get(forKey key: KEY) -> String? {
|
||
return UserDefaults.standard.string(forKey: key.rawValue)
|
||
}
|
||
|
||
static func del(forKey key: KEY) {
|
||
UserDefaults.standard.removeObject(forKey: key.rawValue)
|
||
}
|
||
|
||
static func setArray(forKey key: KEY, value: [String]) {
|
||
UserDefaults.standard.set(value, forKey: key.rawValue)
|
||
}
|
||
|
||
static func getArray(forKey key: KEY) -> [String]? {
|
||
return UserDefaults.standard.array(forKey: key.rawValue) as? [String]
|
||
}
|
||
|
||
static func delArray(forKey key: KEY) {
|
||
UserDefaults.standard.removeObject(forKey: key.rawValue)
|
||
}
|
||
}
|
||
|
||
func getPacUrl() -> String {
|
||
let pacPort = UInt16(UserDefaults.get(forKey: .localPacPort) ?? "11085") ?? 11085
|
||
let pacUrl = "http://127.0.0.1:" + String(pacPort) + "/proxy.js"
|
||
return pacUrl
|
||
}
|
||
|
||
func getConfigUrl() -> String {
|
||
let pacPort = UInt16(UserDefaults.get(forKey: .localPacPort) ?? "11085") ?? 11085
|
||
let configUrl = "http://127.0.0.1:" + String(pacPort) + "/config.json"
|
||
return configUrl
|
||
}
|
||
|
||
extension String {
|
||
// version compare
|
||
func versionToInt() -> [Int] {
|
||
return components(separatedBy: ".")
|
||
.map {
|
||
Int($0) ?? 0
|
||
}
|
||
}
|
||
|
||
//: ### Base64 encoding a string
|
||
func base64Encoded() -> String? {
|
||
if let data = data(using: .utf8) {
|
||
return data.base64EncodedString()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
//: ### Base64 decoding a string
|
||
func base64Decoded() -> String? {
|
||
if let _ = range(of: ":")?.lowerBound {
|
||
return self
|
||
}
|
||
let base64String = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||
let padding = base64String.count + (base64String.count % 4 != 0 ? (4 - base64String.count % 4) : 0)
|
||
if let decodedData = Data(base64Encoded: base64String.padding(toLength: padding, withPad: "=", startingAt: 0), options: NSData.Base64DecodingOptions(rawValue: 0)), let decodedString = NSString(data: decodedData, encoding: String.Encoding.utf8.rawValue) {
|
||
return decodedString as String
|
||
}
|
||
return nil
|
||
}
|
||
|
||
//: isValidUrl
|
||
func isValidUrl() -> Bool {
|
||
let urlRegEx = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"
|
||
let urlTest = NSPredicate(format: "SELF MATCHES %@", urlRegEx)
|
||
let result = urlTest.evaluate(with: self)
|
||
return result
|
||
}
|
||
|
||
// 将原始的url编码为合法的url
|
||
func urlEncoded() -> String {
|
||
let encodeUrlString = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||
return encodeUrlString ?? self
|
||
}
|
||
|
||
// 将编码后的url转换回原始的url
|
||
func urlDecoded() -> String {
|
||
return removingPercentEncoding ?? self
|
||
}
|
||
}
|
||
|
||
// run custom shell
|
||
// demo:
|
||
// shell("/bin/bash",["-c","ls"])
|
||
// shell("/bin/bash",["-c","cd ~ && ls -la"])
|
||
func shell(launchPath: String, arguments: [String]) -> String? {
|
||
do {
|
||
let output = try runCommand(at: launchPath, with: arguments)
|
||
return output
|
||
} catch let error {
|
||
print("shell error: \(error)")
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// → /var/folders/v8/tft1q…/T/…-8DC6DD131DC1/report.pdf
|
||
// guard let tmp = try? TemporaryFile(creatingTempDirectoryForFilename: "v2ray-macos.zip") else {
|
||
// print("err get tmp")
|
||
// return
|
||
// }
|
||
// let fileUrl = tmp.fileURL
|
||
|
||
/// 临时目录中临时文件的包装(Wrapper)。目录是为文件而特别创建的,因此不再需要文件时,可以安全地删除该文件。
|
||
///
|
||
/// 在你不再需要文件时,调用 `deleteDirectory`
|
||
struct TemporaryFile {
|
||
let directoryURL: URL
|
||
let fileURL: URL
|
||
/// 删除临时目录和其中的所有文件。
|
||
let deleteDirectory: () throws -> Void
|
||
/// 使用唯一的名字来创建临时目录,并且使用 `fileURL` 目录中名为 `filename` 的文件来初始化接收者。
|
||
///
|
||
/// - 注意: 这里不会创建文件!
|
||
init(creatingTempDirectoryForFilename filename: String) throws {
|
||
let (directory, deleteDirectory) = try FileManager.default
|
||
.urlForUniqueTemporaryDirectory()
|
||
directoryURL = directory
|
||
fileURL = directory.appendingPathComponent(filename)
|
||
self.deleteDirectory = deleteDirectory
|
||
}
|
||
}
|
||
|
||
extension FileManager {
|
||
/// 创建一个有唯一名字的临时目录并返回 URL。
|
||
///
|
||
/// - 返回:目录 URL 的 tuple 以及删除函数。
|
||
/// 完成后调用函数删除目录。
|
||
///
|
||
/// - 注意: 在应用退出后,不应该存在依赖的临时目录。
|
||
func urlForUniqueTemporaryDirectory(preferredName: String? = nil) throws -> (url: URL, deleteDirectory: () throws -> Void) {
|
||
let basename = preferredName ?? UUID().uuidString
|
||
|
||
var counter = 0
|
||
var createdSubdirectory: URL?
|
||
repeat {
|
||
do {
|
||
let subdirName = counter == 0 ? basename : "\(basename)-\(counter)"
|
||
let subdirectory = temporaryDirectory.appendingPathComponent(subdirName, isDirectory: true)
|
||
try createDirectory(at: subdirectory, withIntermediateDirectories: false)
|
||
createdSubdirectory = subdirectory
|
||
} catch CocoaError.fileWriteFileExists {
|
||
// 捕捉到文件已存在的错误,并使用其他名字重试。
|
||
// 其他错误传播到调用方。
|
||
counter += 1
|
||
}
|
||
} while createdSubdirectory == nil
|
||
|
||
let directory = createdSubdirectory!
|
||
let deleteDirectory: () throws -> Void = {
|
||
try self.removeItem(at: directory)
|
||
}
|
||
return (directory, deleteDirectory)
|
||
}
|
||
}
|
||
|
||
func getAppVersion() -> String {
|
||
return "\(Bundle.main.infoDictionary!["CFBundleShortVersionString"] ?? "")"
|
||
}
|
||
|
||
extension URL {
|
||
func queryParams() -> [String: Any] {
|
||
var dict = [String: Any]()
|
||
|
||
if let components = URLComponents(url: self, resolvingAgainstBaseURL: false) {
|
||
if let queryItems = components.queryItems {
|
||
for item in queryItems {
|
||
dict[item.name] = item.value!
|
||
}
|
||
}
|
||
return dict
|
||
} else {
|
||
return [:]
|
||
}
|
||
}
|
||
}
|
||
|
||
extension utsname {
|
||
static var sMachine: String {
|
||
var utsname = utsname()
|
||
uname(&utsname)
|
||
return withUnsafePointer(to: &utsname.machine) {
|
||
$0.withMemoryRebound(to: CChar.self, capacity: Int(_SYS_NAMELEN)) {
|
||
String(cString: $0)
|
||
}
|
||
}
|
||
}
|
||
|
||
static var isAppleSilicon: Bool {
|
||
sMachine == "arm64"
|
||
}
|
||
}
|
||
|
||
func checkFileIsRootAdmin(file: String) -> Bool {
|
||
do {
|
||
let fileAttrs = try FileManager.default.attributesOfItem(atPath: file)
|
||
var ownerUser = ""
|
||
var groupUser = ""
|
||
for attr in fileAttrs {
|
||
if attr.key.rawValue == "NSFileOwnerAccountName" {
|
||
ownerUser = attr.value as! String
|
||
}
|
||
if attr.key.rawValue == "NSFileGroupOwnerAccountName" {
|
||
groupUser = attr.value as! String
|
||
}
|
||
}
|
||
print("checkFileIsRootAdmin: file=\(file),owner=\(ownerUser),group=\(groupUser)")
|
||
return ownerUser == "root" && groupUser == "admin"
|
||
} catch {
|
||
print("\(error)")
|
||
}
|
||
return false
|
||
}
|
||
|
||
// https://stackoverflow.com/questions/65670932/how-to-find-a-free-local-port-using-swift
|
||
func findFreePort() -> UInt16 {
|
||
var port: UInt16 = 8000
|
||
|
||
let socketFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
|
||
if socketFD == -1 {
|
||
// print("Error creating socket: \(errno)")
|
||
return port
|
||
}
|
||
|
||
var hints = addrinfo(
|
||
ai_flags: AI_PASSIVE,
|
||
ai_family: AF_INET,
|
||
ai_socktype: SOCK_STREAM,
|
||
ai_protocol: 0,
|
||
ai_addrlen: 0,
|
||
ai_canonname: nil,
|
||
ai_addr: nil,
|
||
ai_next: nil
|
||
)
|
||
|
||
var addressInfo: UnsafeMutablePointer<addrinfo>?
|
||
var result = getaddrinfo(nil, "0", &hints, &addressInfo)
|
||
if result != 0 {
|
||
// print("Error getting address info: \(errno)")
|
||
close(socketFD)
|
||
|
||
return port
|
||
}
|
||
|
||
result = Darwin.bind(socketFD, addressInfo!.pointee.ai_addr, socklen_t(addressInfo!.pointee.ai_addrlen))
|
||
if result == -1 {
|
||
// print("Error binding socket to an address: \(errno)")
|
||
close(socketFD)
|
||
|
||
return port
|
||
}
|
||
|
||
result = Darwin.listen(socketFD, 1)
|
||
if result == -1 {
|
||
// print("Error setting socket to listen: \(errno)")
|
||
close(socketFD)
|
||
|
||
return port
|
||
}
|
||
|
||
var addr_in = sockaddr_in()
|
||
addr_in.sin_len = UInt8(MemoryLayout.size(ofValue: addr_in))
|
||
addr_in.sin_family = sa_family_t(AF_INET)
|
||
|
||
var len = socklen_t(addr_in.sin_len)
|
||
result = withUnsafeMutablePointer(to: &addr_in, {
|
||
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||
Darwin.getsockname(socketFD, $0, &len)
|
||
}
|
||
})
|
||
|
||
if result == 0 {
|
||
port = addr_in.sin_port
|
||
}
|
||
|
||
Darwin.shutdown(socketFD, SHUT_RDWR)
|
||
close(socketFD)
|
||
|
||
return port
|
||
}
|
||
|
||
func isPortOpen(port: UInt16) -> Bool {
|
||
do {
|
||
let output = try runCommand(at: "/usr/sbin/lsof", with: ["-i", ":\(port)"])
|
||
NSLog("isPortOpen: \(output)")
|
||
return output.contains("LISTEN")
|
||
} catch let error {
|
||
NSLog("isPortOpen: \(error)")
|
||
}
|
||
return false
|
||
}
|
||
|
||
func getUsablePort(port: UInt16) -> (Bool, UInt16) {
|
||
var i = 0
|
||
var isNew = false
|
||
var _port = port
|
||
while i < 100 {
|
||
let opened = isPortOpen(port: _port)
|
||
NSLog("getUsablePort: try=\(i) port=\(_port) opened=\(opened)")
|
||
if !opened {
|
||
return (isNew, _port)
|
||
}
|
||
isNew = true
|
||
i += 1
|
||
_port += 1
|
||
}
|
||
return (isNew, _port)
|
||
}
|
||
|
||
// can't use this (crash when launchctl)
|
||
func closePort(port: UInt16) {
|
||
let process = Process()
|
||
process.launchPath = "/usr/sbin/lsof"
|
||
process.arguments = ["-ti", ":\(port)"]
|
||
|
||
let pipe = Pipe()
|
||
process.standardOutput = pipe
|
||
|
||
process.terminationHandler = { _ in
|
||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||
if let output = String(data: data, encoding: .utf8) {
|
||
let pids = output.split(separator: "\n")
|
||
for pid in pids {
|
||
if let pid = Int(String(pid)) {
|
||
killProcess(processIdentifier: pid_t(pid))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
process.launch()
|
||
process.waitUntilExit()
|
||
}
|
||
|
||
// get ip address
|
||
|
||
func GetIPAddresses() -> String? {
|
||
var addresses = [String]()
|
||
|
||
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
||
if getifaddrs(&ifaddr) == 0 {
|
||
var ptr = ifaddr
|
||
while ptr != nil {
|
||
let flags = Int32(ptr!.pointee.ifa_flags)
|
||
var addr = ptr!.pointee.ifa_addr.pointee
|
||
if (flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) == (IFF_UP | IFF_RUNNING) {
|
||
if addr.sa_family == UInt8(AF_INET) { // just ipv4
|
||
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||
if getnameinfo(&addr, socklen_t(addr.sa_len), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) == 0 {
|
||
if let address = String(validatingUTF8: hostname) {
|
||
addresses.append(address)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
ptr = ptr!.pointee.ifa_next
|
||
}
|
||
freeifaddrs(ifaddr)
|
||
}
|
||
return addresses.first
|
||
}
|
||
|
||
func killProcess(processIdentifier: pid_t) {
|
||
do {
|
||
let result = kill(processIdentifier, SIGKILL)
|
||
if result == -1 {
|
||
NSLog("killProcess: Failed to kill process with identifier \(processIdentifier)")
|
||
} else {
|
||
NSLog("killProcess: Successfully killed process with identifier \(processIdentifier)")
|
||
}
|
||
}
|
||
}
|
||
|
||
func getProxyUrlSessionConfigure() -> URLSessionConfiguration {
|
||
// Create a URLSessionConfiguration with proxy settings
|
||
let configuration = URLSessionConfiguration.default
|
||
// v2ray is running
|
||
if UserDefaults.getBool(forKey: .v2rayTurnOn) {
|
||
let proxyHost = "127.0.0.1"
|
||
let proxyPort = getHttpProxyPort()
|
||
// set proxies
|
||
configuration.connectionProxyDictionary = [
|
||
kCFNetworkProxiesHTTPEnable as AnyHashable: true,
|
||
kCFNetworkProxiesHTTPProxy as AnyHashable: proxyHost,
|
||
kCFNetworkProxiesHTTPPort as AnyHashable: proxyPort,
|
||
kCFNetworkProxiesHTTPSEnable as AnyHashable: true,
|
||
kCFNetworkProxiesHTTPSProxy as AnyHashable: proxyHost,
|
||
kCFNetworkProxiesHTTPSPort as AnyHashable: proxyPort,
|
||
]
|
||
}
|
||
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||
configuration.urlCache = nil
|
||
configuration.timeoutIntervalForRequest = 30 // Set your desired timeout interval in seconds
|
||
|
||
return configuration
|
||
}
|
||
|
||
func getProxyUrlSessionConfigure(httpProxyPort: uint16) -> URLSessionConfiguration {
|
||
// Create a URLSessionConfiguration with proxy settings
|
||
let configuration = URLSessionConfiguration.default
|
||
let proxyHost = "127.0.0.1"
|
||
let proxyPort = httpProxyPort
|
||
// set proxies
|
||
configuration.connectionProxyDictionary = [
|
||
kCFNetworkProxiesHTTPEnable as AnyHashable: true,
|
||
kCFNetworkProxiesHTTPProxy as AnyHashable: proxyHost,
|
||
kCFNetworkProxiesHTTPPort as AnyHashable: proxyPort,
|
||
kCFNetworkProxiesHTTPSEnable as AnyHashable: true,
|
||
kCFNetworkProxiesHTTPSProxy as AnyHashable: proxyHost,
|
||
kCFNetworkProxiesHTTPSPort as AnyHashable: proxyPort,
|
||
]
|
||
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||
configuration.urlCache = nil
|
||
configuration.timeoutIntervalForRequest = 2 // Set your desired timeout interval in seconds
|
||
return configuration
|
||
}
|
||
|
||
func getHttpProxyPort() -> UInt16 {
|
||
return UInt16(UserDefaults.get(forKey: .localHttpPort) ?? "1087") ?? 1087
|
||
}
|
||
|
||
func getSocksProxyPort() -> UInt16 {
|
||
return UInt16(UserDefaults.get(forKey: .localSockPort) ?? "1080") ?? 1080
|
||
}
|
||
|
||
func getPacPort() -> UInt16 {
|
||
return UInt16(UserDefaults.get(forKey: .localPacPort) ?? "11085") ?? 11085
|
||
}
|
||
|
||
func killAllPing() {
|
||
let pskillCmd = "ps aux | grep v2ray | grep '.V2rayU/.config.' | awk '{print $2}' | xargs kill"
|
||
let msg = shell(launchPath: "/bin/bash", arguments: ["-c", pskillCmd])
|
||
NSLog("killAllPing: \(String(describing: msg))")
|
||
let rmPingJsonCmd = "rm -f ~/.V2rayU/.config.*.json"
|
||
let msg1 = shell(launchPath: "/bin/bash", arguments: ["-c", rmPingJsonCmd])
|
||
NSLog("rmPingJson: \(String(describing: msg1))")
|
||
}
|
||
|
||
func killSelfV2ray() {
|
||
let pskillCmd = "ps aux | grep v2ray | grep '.V2rayU/config.json' | awk '{print $2}' | xargs kill"
|
||
let msg = shell(launchPath: "/bin/bash", arguments: ["-c", pskillCmd])
|
||
NSLog("killSelfV2ray: \(String(describing: msg))")
|
||
}
|
||
|
||
func OpenLogs() {
|
||
if !FileManager.default.fileExists(atPath: logFilePath) {
|
||
let txt = ""
|
||
try! txt.write(to: URL(fileURLWithPath: logFilePath), atomically: true, encoding: String.Encoding.utf8)
|
||
}
|
||
|
||
let task = Process.launchedProcess(launchPath: "/usr/bin/open", arguments: [logFilePath])
|
||
task.waitUntilExit()
|
||
if task.terminationStatus == 0 {
|
||
NSLog("open logs succeeded.")
|
||
} else {
|
||
NSLog("open logs failed.")
|
||
}
|
||
}
|
||
|
||
func ClearLogs() {
|
||
let txt = ""
|
||
try! txt.write(to: URL(fileURLWithPath: logFilePath), atomically: true, encoding: String.Encoding.utf8)
|
||
}
|
||
|
||
func showDock(state: Bool) {
|
||
DispatchQueue.main.async {
|
||
// Get transform state.
|
||
var transformState: ProcessApplicationTransformState
|
||
if state {
|
||
transformState = ProcessApplicationTransformState(kProcessTransformToForegroundApplication)
|
||
} else {
|
||
transformState = ProcessApplicationTransformState(kProcessTransformToUIElementApplication)
|
||
}
|
||
|
||
// Show / hide dock icon.
|
||
var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess))
|
||
TransformProcessType(&psn, transformState)
|
||
if state {
|
||
// bring to front
|
||
NSApp.activate(ignoringOtherApps: true)
|
||
}
|
||
}
|
||
}
|
||
|
||
func noticeTip(title: String = "", informativeText: String = "") {
|
||
makeToast(message: title + " : " + informativeText)
|
||
}
|
||
|
||
enum CommandExecutionError: Error {
|
||
case fileNotFound(String)
|
||
case insufficientPermissions(String)
|
||
case unknown(Error)
|
||
}
|
||
|
||
func runCommand(at path: String, with arguments: [String]) throws -> String {
|
||
let process = Process()
|
||
process.executableURL = URL(fileURLWithPath: path)
|
||
process.arguments = arguments
|
||
|
||
let pipe = Pipe()
|
||
process.standardOutput = pipe
|
||
process.standardError = pipe
|
||
|
||
do {
|
||
try process.run()
|
||
process.waitUntilExit()
|
||
|
||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||
if let output = String(data: data, encoding: .utf8) {
|
||
return output
|
||
} else {
|
||
return ""
|
||
}
|
||
} catch {
|
||
if (error as NSError).domain == NSCocoaErrorDomain {
|
||
switch (error as NSError).code {
|
||
case NSFileNoSuchFileError:
|
||
throw CommandExecutionError.fileNotFound(path)
|
||
case NSFileReadNoPermissionError:
|
||
throw CommandExecutionError.insufficientPermissions(path)
|
||
default:
|
||
throw CommandExecutionError.unknown(error)
|
||
}
|
||
} else {
|
||
throw CommandExecutionError.unknown(error)
|
||
}
|
||
}
|
||
}
|