Files
Archive/v2rayu/V2rayU/V2raySubscription.swift
2024-03-05 02:32:38 -08:00

546 lines
17 KiB
Swift

//
// V2raySubscription.swift
// V2rayU
//
// Created by yanue on 2019/5/15.
// Copyright © 2019 yanue. All rights reserved.
//
import Cocoa
import Alamofire
import SwiftyJSON
import Yams
// ----- v2ray subscribe manager -----
class V2raySubscription: NSObject {
static var shared = V2raySubscription()
static let lock = NSLock()
// Initialization
override init() {
super.init()
print("V2raySubscription init")
V2raySubscription.loadConfig()
}
// v2ray subscribe list
static private var v2raySubList: [V2raySubItem] = []
// (init) load v2ray subscribe list from UserDefaults
static func loadConfig() {
self.lock.lock()
defer {
self.lock.unlock()
}
// static reset
self.v2raySubList = []
// load name list from UserDefaults
let list = UserDefaults.getArray(forKey: .v2raySubList)
// print("loadConfig", list)
if list == nil {
return
}
// load each V2raySubItem
for item in list! {
guard let v2ray = V2raySubItem.load(name: item) else {
// delete from UserDefaults
V2raySubItem.remove(name: item)
continue
}
// append
self.v2raySubList.append(v2ray)
}
}
// get list from v2ray subscribe list
static func list() -> [V2raySubItem] {
return self.v2raySubList
}
// get count from v2ray subscribe list
static func count() -> Int {
return self.v2raySubList.count
}
static func edit(rowIndex: Int, remark: String) {
if !self.v2raySubList.indices.contains(rowIndex) {
NSLog("index out of range", rowIndex)
return
}
// update list
self.v2raySubList[rowIndex].remark = remark
// save
let v2ray = self.v2raySubList[rowIndex]
v2ray.remark = remark
v2ray.store()
}
static func edit(rowIndex: Int, url: String) {
if !self.v2raySubList.indices.contains(rowIndex) {
NSLog("index out of range", rowIndex)
return
}
// update list
self.v2raySubList[rowIndex].url = url
// save
let v2ray = self.v2raySubList[rowIndex]
v2ray.url = url
v2ray.store()
}
// move item to new index
static func move(oldIndex: Int, newIndex: Int) {
if !V2raySubscription.v2raySubList.indices.contains(oldIndex) {
NSLog("index out of range", oldIndex)
return
}
if !V2raySubscription.v2raySubList.indices.contains(newIndex) {
NSLog("index out of range", newIndex)
return
}
let o = self.v2raySubList[oldIndex]
self.v2raySubList.remove(at: oldIndex)
self.v2raySubList.insert(o, at: newIndex)
// update subscribe list UserDefaults
self.saveItemList()
}
// add v2ray subscribe (by scan qrcode)
static func add(remark: String, url: String) {
if self.v2raySubList.count > 50 {
// NSLog("over max len")
// return
}
// name is : subscribe. + uuid
let name = "subscribe." + UUID().uuidString
let v2ray = V2raySubItem(name: name, remark: remark, url: url)
// save to v2ray UserDefaults
v2ray.store()
// just add to mem
self.v2raySubList.append(v2ray)
// update subscribe list UserDefaults
self.saveItemList()
}
// remove v2ray subscribe (tmp and UserDefaults and config json file)
static func remove(idx: Int) {
if !V2raySubscription.v2raySubList.indices.contains(idx) {
NSLog("index out of range", idx)
return
}
let v2ray = V2raySubscription.v2raySubList[idx]
// delete from tmp
self.v2raySubList.remove(at: idx)
// delete from v2ray UserDefaults
V2raySubItem.remove(name: v2ray.name)
// update subscribe list UserDefaults
self.saveItemList()
}
// update subscribe list UserDefaults
static private func saveItemList() {
var v2raySubList: Array<String> = []
for item in V2raySubscription.list() {
v2raySubList.append(item.name)
}
UserDefaults.setArray(forKey: .v2raySubList, value: v2raySubList)
}
// load json file data
static func loadSubItem(idx: Int) -> V2raySubItem? {
if !V2raySubscription.v2raySubList.indices.contains(idx) {
NSLog("index out of range", idx)
return nil
}
return self.v2raySubList[idx]
}
}
// ----- v2ray subscribe item -----
class V2raySubItem: NSObject, NSCoding {
var name: String
var remark: String
var isValid: Bool
var url: String
// init
required init(name: String, remark: String, url: String, isValid: Bool = true) {
self.name = name
self.remark = remark
self.isValid = isValid
self.url = url
}
// decode
required init(coder decoder: NSCoder) {
self.name = decoder.decodeObject(forKey: "Name") as? String ?? ""
self.remark = decoder.decodeObject(forKey: "Remark") as? String ?? ""
self.isValid = decoder.decodeBool(forKey: "IsValid")
self.url = decoder.decodeObject(forKey: "Url") as? String ?? ""
}
// object encode
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "Name")
coder.encode(remark, forKey: "Remark")
coder.encode(isValid, forKey: "IsValid")
coder.encode(url, forKey: "Url")
}
// store into UserDefaults
func store() {
let modelData = NSKeyedArchiver.archivedData(withRootObject: self)
UserDefaults.standard.set(modelData, forKey: self.name)
}
// static load from UserDefaults
static func load(name: String) -> V2raySubItem? {
guard let myModelData = UserDefaults.standard.data(forKey: name) else {
return nil
}
do {
let result = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(myModelData)
return result as? V2raySubItem
} catch {
print("load userDefault error:", error)
return nil
}
}
// remove from UserDefaults
static func remove(name: String) {
UserDefaults.standard.removeObject(forKey: name)
}
}
// ----- v2ray subscribe updater -----
let NOTIFY_UPDATE_SubSync = Notification.Name(rawValue: "NOTIFY_UPDATE_SubSync")
class V2raySubSync: NSObject {
let lock = NSLock()
var V2raySubSyncing = false
var group = DispatchGroup()
static var shared = V2raySubSync()
// Initialization
override init() {
super.init()
NSLog("V2raySubSync init")
}
// sync from Subscription list
public func sync() {
if V2raySubSyncing {
NSLog("V2raySubSync Syncing ...")
return
}
self.V2raySubSyncing = true
NSLog("V2raySubSync start")
let thread = Thread {
V2raySubscription.loadConfig()
let list = V2raySubscription.list()
if list.count == 0 {
self.logTip(title: "fail: ", uri: "", informativeText: " please add Subscription Url")
}
// sync queue with DispatchGroup
self.group = DispatchGroup()
let subQueue = DispatchQueue(label: "subQueue", qos: .background)
for item in list {
subQueue.sync {
self.group.enter()
self.dlFromUrl(url: item.url, subscribe: item.name)
}
}
self.group.wait()
NSLog("V2raySubSync end")
self.refreshMenu()
}
thread.start()
}
func refreshMenu() {
NSLog("V2raySubSync refreshMenu")
self.V2raySubSyncing = false
usleep(useconds_t(1 * second))
do {
// refresh server
DispatchQueue.main.async {
menuController.showServers()
}
usleep(useconds_t(2 * second))
// do ping
ping.pingAll()
}
}
func importEnd(url: String, subscribe: String) {
self.group.leave()
}
public func dlFromUrl(url: String, subscribe: String) {
logTip(title: "loading from : ", uri: "", informativeText: url + "\n\n")
guard let reqUrl = URL(string: url) else {
logTip(title: "loading from : ", uri: "", informativeText: "url is not valid: " + url + "\n\n")
self.importEnd(url: url, subscribe: subscribe)
return
}
// url request with proxy
let session = URLSession(configuration: getProxyUrlSessionConfigure())
let task = session.dataTask(with: URLRequest(url: reqUrl)){(data: Data?, response: URLResponse?, error: Error?) in
defer {
self.importEnd(url: url, subscribe: subscribe)
}
if error != nil {
self.logTip(title: "loading fail: ", uri: url, informativeText: "error: \(String(describing: error))")
} else {
if data != nil {
if let outputStr = String(data: data!, encoding: String.Encoding.utf8) {
self.handle(base64Str: outputStr, subscribe: subscribe, url: url)
} else {
self.logTip(title: "loading fail: ", uri: url, informativeText: "data is nil")
}
} else {
self.logTip(title: "loading fail: ", uri: url, informativeText: "data is nil")
}
}
}
task.resume()
}
func handle(base64Str: String, subscribe: String, url: String) {
guard let strTmp = base64Str.trimmingCharacters(in: .whitespacesAndNewlines).base64Decoded() else {
self.logTip(title: "parse fail : ", uri: "", informativeText: base64Str)
return
}
self.logTip(title: "handle url: ", uri: "", informativeText: url + "\n\n")
if self.importByYaml(strTmp: strTmp, subscribe: subscribe) {
return
}
self.importByNormal(strTmp: strTmp, subscribe: subscribe)
}
func getOld(subscribe: String) -> [String] {
// reload all
V2rayServer.loadConfig()
// get old
var oldList: [String] = []
for (_, item) in V2rayServer.list().enumerated() {
if item.subscribe == subscribe {
oldList.append(item.name)
}
}
return oldList
}
func importByYaml(strTmp: String, subscribe: String) -> Bool {
// parse clash yaml
do {
let oldList = getOld(subscribe: subscribe)
var exists: Dictionary = [String: Bool]()
let decoder = YAMLDecoder()
let decoded = try decoder.decode(Clash.self, from: strTmp)
for item in decoded.proxies {
if let importUri = importByClash(clash: item) {
importUri.remark = item.name
if let v2rayOld = self.saveImport(importUri: importUri, subscribe: subscribe) {
exists[v2rayOld.name] = true
}
}
}
logTip(title: "need remove?: ", informativeText: "old=\(oldList.count) - new=\(exists.count)")
// remove not exist
for name in oldList {
if !(exists[name] ?? false) {
// delete from v2ray UserDefaults
V2rayItem.remove(name: name)
logTip(title: "remove: ", informativeText: name)
}
}
return true
} catch {
NSLog("parseYaml \(error)")
}
return false
}
func importByNormal(strTmp: String, subscribe: String) {
let oldList = getOld(subscribe: subscribe)
var exists: Dictionary = [String: Bool]()
let list = strTmp.trimmingCharacters(in: .newlines).components(separatedBy: CharacterSet.newlines)
var count = 0
for uri in list {
count += 1
if count > 50 {
break // limit 50
}
// import every server
if (uri.count > 0) {
let filterUri = uri.trimmingCharacters(in: .whitespacesAndNewlines)
if let importUri = ImportUri.importUri(uri: filterUri,checkExist: false) {
if let v2rayOld = self.saveImport(importUri: importUri, subscribe: subscribe) {
exists[v2rayOld.name] = true
}
}
}
}
logTip(title: "need remove?: ", informativeText: "old=\(oldList.count) - new=\(exists.count)")
// remove not exist
for name in oldList {
if !(exists[name] ?? false) {
// delete from v2ray UserDefaults
V2rayItem.remove(name: name)
logTip(title: "remove: ", informativeText: name)
}
}
}
func saveImport(importUri: ImportUri, subscribe: String) -> V2rayItem? {
if importUri.isValid {
var newUri = importUri.uri
// clash has no uri
if newUri.count == 0 {
// old share uri
let v2ray = V2rayItem(name: "tmp", remark: importUri.remark, isValid: importUri.isValid, json: importUri.json, url: "", subscribe: subscribe)
let share = ShareUri()
share.qrcode(item: v2ray)
newUri = share.uri
}
print("\(importUri.remark) - \(newUri)")
if let v2rayOld = V2rayServer.existItem(url: newUri) {
v2rayOld.json = importUri.json
v2rayOld.isValid = importUri.isValid
v2rayOld.remark = importUri.remark
v2rayOld.store()
logTip(title: "success update: ", informativeText: importUri.remark)
return v2rayOld
} else {
// add server
V2rayServer.add(remark: importUri.remark, json: importUri.json, isValid: true, url: newUri, subscribe: subscribe)
logTip(title: "success add: ", informativeText: importUri.remark)
}
} else {
logTip(title: "fail: ", informativeText: importUri.error)
}
return nil
}
func logTip(title: String = "", uri: String = "", informativeText: String = "") {
NotificationCenter.default.post(name: NOTIFY_UPDATE_SubSync, object: title + informativeText + "\n")
print("SubSync", title + informativeText)
if uri != "" {
NotificationCenter.default.post(name: NOTIFY_UPDATE_SubSync, object: "url: " + uri + "\n\n\n")
}
}
}
// MARK: - clash
struct Clash: Codable {
var port, socksPort, redirPort, mixedPort: Int?
var allowLAN: Bool?
var mode: String
var logLevel: String?
var externalController: String?
var proxies: [clashProxy]
var rules: [String]?
}
// MARK: - Proxy
struct clashProxy: Codable {
var type: String
var name: String
var server: String
var port: Int
var username: String? // socks5 | http
var password: String?
var sni: String?
var skipCERTVerify: Bool?
var cipher: String? // ss | ssr
var uuid: String? // vmess | vless
var alterId: Int? // vmess | vless
var tls: Bool? // tls
var fp: String?
var `protocol`: String? // ssr
var obfs: String? // ssr
var udp: Bool? // socks5
var network: String? // ws | h2
var servername: String? // priority over wss host, REALITY servername,SNI
var clientFingerprint: String? // vless
var fingerprint: String? // vmess
var security: String? // vmess
var flow: String? // vless
var wsOpts: clashWsOpts? // vmess
var httpOpts: clashHttpOpts? // vmess
var h2Opts: clashH2Opts? // vmess
var grpcOpts: grpcOpts? // vmess
var realityOpts: realityOpts? // vless
}
struct clashWsOpts: Codable {
var path: String?
}
struct clashHttpOpts: Codable {
var path: [String]?
}
struct clashH2Opts: Codable {
var path: String?
var host: [String]?
}
struct grpcOpts: Codable {
var grpcServiceName: String?
}
struct realityOpts: Codable {
var publicKey: String?
var shortId: String?
}
/**
- {"type":"ss","name":"v2rayse_test_1","server":"198.57.27.218","port":5004,"cipher":"aes-256-gcm","password":"g5MeD6Ft3CWlJId"}
- {"type":"ssr","name":"v2rayse_test_3","server":"20.239.49.44","port":59814,"protocol":"origin","cipher":"dummy","obfs":"plain","password":"3df57276-03ef-45cf-bdd4-4edb6dfaa0ef"}
- {"type":"vmess","name":"v2rayse_test_2","ws-opts":{"path":"/"},"server":"154.23.190.162","port":443,"uuid":"b9984674-f771-4e67-a198-","alterId":"0","cipher":"auto","network":"ws"}
- {"type":"vless","name":"test","server":"1.2.3.4","port":7777,"uuid":"abc-def-ghi-fge-zsx","skip-cert-verify":true,"network":"tcp","tls":true,"udp":true}
- {"type":"trojan","name":"v2rayse_test_4","server":"ca-trojan.bonds.id","port":443,"password":"bc7593fe-0604-4fbe--b4ab-11eb-b65e-1239d0255272","udp":true,"skip-cert-verify":true}
- {"type":"http","name":"http_proxy","server":"124.15.12.24","port":251,"username":"username","password":"password","udp":true}
- {"type":"socks5","name":"socks5_proxy","server":"124.15.12.24","port":2312,"udp":true}
- {"type":"socks5","name":"telegram_proxy","server":"1.2.3.4","port":123,"username":"username","password":"password","udp":true}
*/