Files
Archive/v2rayu/V2rayU/Util.swift
2024-07-18 20:32:03 +02:00

611 lines
19 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
// urlurl
func urlEncoded() -> String {
let encodeUrlString = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
return encodeUrlString ?? self
}
// urlurl
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)
}
}
}