mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
544 lines
18 KiB
Swift
544 lines
18 KiB
Swift
//
|
||
// V2raySubscription.swift
|
||
// V2rayU
|
||
//
|
||
// Created by yanue on 2019/5/15.
|
||
// Copyright © 2019 yanue. All rights reserved.
|
||
//
|
||
|
||
import Cocoa
|
||
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 {
|
||
var V2raySubSyncing = false
|
||
let maxConcurrentTasks = 1 // work pool
|
||
|
||
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 list = V2raySubscription.list()
|
||
|
||
if list.count == 0 {
|
||
self.logTip(title: "fail: ", uri: "", informativeText: " please add Subscription Url")
|
||
}
|
||
// sync queue with DispatchGroup
|
||
Task {
|
||
do {
|
||
try await self.syncTaskGroup(items: list)
|
||
} catch let error {
|
||
NSLog("pingTaskGroup error: \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
func syncTaskGroup(items: [V2raySubItem]) async throws {
|
||
let taskChunks = stride(from: 0, to: items.count, by: maxConcurrentTasks).map {
|
||
Array(items[$0..<min($0 + maxConcurrentTasks, items.count)])
|
||
}
|
||
NSLog("syncTaskGroup-start: taskChunks=\(taskChunks.count)")
|
||
for (i, chunk) in taskChunks.enumerated() {
|
||
NSLog("syncTaskGroup-start-\(i): count=\(chunk.count)")
|
||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||
for item in chunk {
|
||
group.addTask {
|
||
do {
|
||
try await self.dlFromUrl(url: item.url, subscribe: item.name)
|
||
} catch {
|
||
NSLog("dlFromUrl error: \(error)")
|
||
}
|
||
return
|
||
}
|
||
}
|
||
// 等待当前批次所有任务完成
|
||
try await group.waitForAll()
|
||
}
|
||
NSLog("syncTaskGroup-end-\(i)")
|
||
}
|
||
NSLog("syncTaskGroup-end")
|
||
self.refreshMenu()
|
||
}
|
||
|
||
func refreshMenu() {
|
||
NSLog("V2raySubSync refreshMenu")
|
||
self.V2raySubSyncing = false
|
||
usleep(useconds_t(1 * second))
|
||
do {
|
||
// refresh server
|
||
menuController.showServers()
|
||
// sleep 2
|
||
sleep(2)
|
||
// do ping
|
||
ping.pingAll()
|
||
}
|
||
}
|
||
|
||
public func dlFromUrl(url: String, subscribe: String) async throws {
|
||
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")
|
||
return
|
||
}
|
||
|
||
// url request with proxy
|
||
let session = URLSession(configuration: getProxyUrlSessionConfigure())
|
||
do {
|
||
let (data, _) = try await session.data(for: URLRequest(url: reqUrl))
|
||
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")
|
||
}
|
||
} catch let error {
|
||
// failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding
|
||
NSLog("save json file fail: \(error)")
|
||
}
|
||
}
|
||
|
||
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 {
|
||
// 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}
|
||
*/
|