2018-08-05 18:34:12 +08:00
|
|
|
//
|
|
|
|
// ConfigFileFactory.swift
|
|
|
|
// ClashX
|
|
|
|
//
|
|
|
|
// Created by CYC on 2018/8/5.
|
2018-08-08 13:47:38 +08:00
|
|
|
// Copyright © 2018年 yichengchen. All rights reserved.
|
2018-08-05 18:34:12 +08:00
|
|
|
//
|
|
|
|
import Foundation
|
2018-08-05 23:16:58 +08:00
|
|
|
import AppKit
|
2018-08-06 14:17:04 +08:00
|
|
|
import SwiftyJSON
|
2018-10-07 21:03:16 +08:00
|
|
|
import Yams
|
2018-08-05 18:34:12 +08:00
|
|
|
|
|
|
|
class ConfigFileFactory {
|
2018-08-12 00:10:42 +08:00
|
|
|
static let shared = ConfigFileFactory()
|
|
|
|
var witness:Witness?
|
|
|
|
func watchConfigFile() {
|
2018-10-14 22:48:51 +08:00
|
|
|
let path = (NSHomeDirectory() as NSString).appendingPathComponent("/.config/clash/config.yml")
|
2018-08-12 00:10:42 +08:00
|
|
|
witness = Witness(paths: [path], flags: .FileEvents, latency: 0.3) { events in
|
2018-08-19 11:14:16 +08:00
|
|
|
for event in events {
|
|
|
|
print(event.flags)
|
|
|
|
if event.flags.contains(.ItemModified) || event.flags.contains(.ItemCreated){
|
|
|
|
NSUserNotificationCenter.default.postConfigFileChangeDetectionNotice()
|
2018-08-29 20:01:54 +08:00
|
|
|
NotificationCenter.default.post(Notification(name: kConfigFileChange))
|
2018-08-19 11:14:16 +08:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2018-08-12 00:10:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-14 22:48:51 +08:00
|
|
|
|
2018-08-26 13:25:29 +08:00
|
|
|
|
2018-10-20 14:27:39 +08:00
|
|
|
static func configs(from proxyModels:[ProxyServerModel]) -> [String:Any]? {
|
|
|
|
guard let yamlStr = try? String(contentsOfFile: kConfigFilePath),
|
|
|
|
var yaml = (try? Yams.load(yaml: yamlStr)) as? [String:Any] else {return nil}
|
2018-10-14 22:48:51 +08:00
|
|
|
|
2018-10-20 14:27:39 +08:00
|
|
|
var proxies:[Any] = yaml["Proxy"] as? [Any] ?? []
|
|
|
|
var proxyNames = [String]()
|
|
|
|
for each in proxyModels {
|
|
|
|
var newProxy:[String : Any] = ["name":each.remark,
|
|
|
|
"server":each.serverHost,
|
|
|
|
"port":Int(each.serverPort) ?? 0,
|
|
|
|
]
|
|
|
|
|
|
|
|
switch each.proxyType {
|
|
|
|
case .shadowsocks:
|
|
|
|
newProxy["type"] = "ss"
|
|
|
|
newProxy["cipher"] = each.method
|
|
|
|
newProxy["password"] = each.password
|
|
|
|
if (each.simpleObfs != .none) {
|
|
|
|
newProxy["obfs"] = each.simpleObfs.rawValue
|
|
|
|
newProxy["obfs-host"] = "bing.com"
|
|
|
|
}
|
|
|
|
case .socks5:
|
|
|
|
newProxy["type"] = "socks"
|
|
|
|
}
|
|
|
|
proxies.append(newProxy)
|
|
|
|
proxyNames.append(each.remark)
|
|
|
|
}
|
|
|
|
yaml["Proxy"] = proxies
|
|
|
|
|
|
|
|
var proxyGroups = yaml["Proxy Group"] as? [Any] ?? []
|
|
|
|
if proxyGroups.count == 0 {
|
|
|
|
|
|
|
|
let autoGroup:[String : Any] = ["name":"auto","type": "url-test", "url": "https://www.bing.com", "interval": 300,"proxies":proxyNames]
|
|
|
|
proxyNames.append("auto")
|
|
|
|
let selectGroup:[String : Any] = ["name":"Proxy","type":"select","proxies":proxyNames]
|
|
|
|
proxyGroups = [autoGroup,selectGroup]
|
|
|
|
yaml["Proxy Group"] = proxyGroups
|
|
|
|
}
|
|
|
|
|
|
|
|
return yaml
|
2018-08-05 18:34:12 +08:00
|
|
|
}
|
|
|
|
|
2018-10-14 22:48:51 +08:00
|
|
|
static func saveToClashConfigFile(config:[String:Any]) {
|
|
|
|
// save to ~/.config/clash/config.yml
|
2018-09-22 17:18:25 +08:00
|
|
|
_ = self.backupAndRemoveConfigFile(showAlert: false)
|
2018-10-20 10:31:07 +08:00
|
|
|
var config = config
|
|
|
|
var finalConfigString = ""
|
|
|
|
do {
|
|
|
|
if let proxyConfig = config["Proxy"] {
|
|
|
|
finalConfigString += try
|
|
|
|
Yams.dump(object: ["Proxy":proxyConfig],allowUnicode:true)
|
|
|
|
config["Proxy"] = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if let proxyGroupConfig = config["Proxy Group"] {
|
|
|
|
finalConfigString += try
|
|
|
|
Yams.dump(object: ["Proxy Group":proxyGroupConfig]
|
|
|
|
,allowUnicode:true)
|
|
|
|
config["Proxy Group"] = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if let rule = config["Rule"] {
|
|
|
|
finalConfigString += try
|
|
|
|
Yams.dump(object: ["Rule":rule],allowUnicode:true)
|
|
|
|
config["Rule"] = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
finalConfigString = try Yams.dump(object: config,allowUnicode:true) + finalConfigString
|
|
|
|
|
|
|
|
try finalConfigString.write(toFile: kConfigFilePath, atomically: true, encoding: .utf8)
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-09-01 14:19:53 +08:00
|
|
|
}
|
|
|
|
|
2018-09-27 23:07:05 +08:00
|
|
|
@discardableResult
|
2018-09-05 19:30:38 +08:00
|
|
|
static func backupAndRemoveConfigFile(showAlert:Bool = false) -> Bool {
|
2018-09-01 14:19:53 +08:00
|
|
|
let path = kConfigFilePath
|
2018-10-14 23:42:53 +08:00
|
|
|
|
2018-08-05 18:34:12 +08:00
|
|
|
if (FileManager.default.fileExists(atPath: path)) {
|
2018-10-14 23:42:53 +08:00
|
|
|
if (showAlert) {
|
|
|
|
if !self.showReplacingConfigFileAlert() {return false}
|
|
|
|
}
|
2018-10-14 22:48:51 +08:00
|
|
|
let newPath = "\(kConfigFolderPath)config_\(Date().timeIntervalSince1970).yml"
|
2018-09-01 14:19:53 +08:00
|
|
|
try? FileManager.default.moveItem(atPath: path, toPath: newPath)
|
2018-08-05 18:34:12 +08:00
|
|
|
}
|
2018-09-05 19:30:38 +08:00
|
|
|
return true
|
2018-08-05 23:16:58 +08:00
|
|
|
}
|
|
|
|
|
2018-10-14 22:48:51 +08:00
|
|
|
static func copySampleConfigIfNeed() {
|
|
|
|
if !FileManager.default.fileExists(atPath: kConfigFilePath) {
|
2018-10-14 23:42:53 +08:00
|
|
|
_ = replaceConfigWithSampleConfig()
|
2018-10-14 22:48:51 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static func replaceConfigWithSampleConfig() -> Bool {
|
2018-09-05 19:30:38 +08:00
|
|
|
if (!backupAndRemoveConfigFile(showAlert: true)) {
|
2018-09-05 20:19:47 +08:00
|
|
|
return false
|
2018-09-05 19:30:38 +08:00
|
|
|
}
|
2018-10-14 22:48:51 +08:00
|
|
|
let path = Bundle.main.path(forResource: "sampleConfig", ofType: "yml")!
|
2018-09-04 13:32:33 +08:00
|
|
|
try? FileManager.default.copyItem(atPath: path, toPath: kConfigFilePath)
|
2018-08-11 23:07:51 +08:00
|
|
|
NSUserNotificationCenter.default.postGenerateSimpleConfigNotice()
|
2018-09-05 20:19:47 +08:00
|
|
|
return true
|
2018-08-11 23:07:51 +08:00
|
|
|
}
|
|
|
|
|
2018-08-05 23:16:58 +08:00
|
|
|
|
|
|
|
static func importConfigFile() {
|
|
|
|
let openPanel = NSOpenPanel()
|
|
|
|
openPanel.title = "Choose Config Json File"
|
|
|
|
openPanel.allowsMultipleSelection = false
|
|
|
|
openPanel.canChooseDirectories = false
|
|
|
|
openPanel.canCreateDirectories = false
|
|
|
|
openPanel.canChooseFiles = true
|
|
|
|
openPanel.becomeKey()
|
|
|
|
let result = openPanel.runModal()
|
2018-08-06 14:17:04 +08:00
|
|
|
guard (result.rawValue == NSFileHandlingPanelOKButton && (openPanel.url) != nil) else {
|
|
|
|
NSUserNotificationCenter.default
|
|
|
|
.post(title: "Import Server Profile failed!",
|
|
|
|
info: "Invalid config file!")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let fileManager = FileManager.default
|
|
|
|
let filePath:String = (openPanel.url?.path)!
|
|
|
|
var profiles = [ProxyServerModel]()
|
|
|
|
|
|
|
|
|
|
|
|
if fileManager.fileExists(atPath: filePath) &&
|
|
|
|
filePath.hasSuffix("json") {
|
|
|
|
if let data = fileManager.contents(atPath: filePath),
|
|
|
|
let json = try? JSON(data: data) {
|
|
|
|
let remarkSet = Set<String>()
|
|
|
|
for item in json["configs"].arrayValue{
|
|
|
|
if let host = item["server"].string,
|
|
|
|
let method = item["method"].string,
|
|
|
|
let password = item["password"].string{
|
|
|
|
|
|
|
|
let profile = ProxyServerModel()
|
|
|
|
profile.serverHost = host
|
|
|
|
profile.serverPort = String(item["server_port"].intValue)
|
|
|
|
profile.method = method
|
|
|
|
profile.password = password
|
|
|
|
profile.remark = item["remarks"].stringValue
|
2018-09-23 20:33:34 +08:00
|
|
|
profile.pluginStr = item["plugin_opts"].stringValue
|
2018-08-06 14:17:04 +08:00
|
|
|
if remarkSet.contains(profile.remark) {
|
|
|
|
profile.remark.append("Dup")
|
|
|
|
}
|
|
|
|
|
|
|
|
if (profile.isValid()) {
|
|
|
|
profiles.append(profile)
|
|
|
|
}
|
2018-08-06 00:00:32 +08:00
|
|
|
}
|
2018-08-05 23:16:58 +08:00
|
|
|
}
|
|
|
|
|
2018-08-06 14:17:04 +08:00
|
|
|
if (profiles.count > 0) {
|
2018-10-20 14:27:39 +08:00
|
|
|
if let configDict = configs(from: profiles) {
|
|
|
|
self.saveToClashConfigFile(config: configDict)
|
|
|
|
NSUserNotificationCenter
|
|
|
|
.default
|
|
|
|
.post(title: "Import Server Profile succeed!",
|
|
|
|
info: "Successful import \(profiles.count) items")
|
|
|
|
NotificationCenter.default.post(Notification(name: kShouldUpDateConfig))
|
|
|
|
}
|
|
|
|
|
2018-08-06 14:17:04 +08:00
|
|
|
} else {
|
|
|
|
NSUserNotificationCenter
|
|
|
|
.default
|
|
|
|
.post(title: "Import Server Profile Fail!",
|
|
|
|
info: "No proxies are imported")
|
|
|
|
}
|
2018-08-05 23:16:58 +08:00
|
|
|
}
|
|
|
|
}
|
2018-08-06 14:17:04 +08:00
|
|
|
|
2018-08-05 18:34:12 +08:00
|
|
|
}
|
2018-08-26 13:25:29 +08:00
|
|
|
|
|
|
|
static func addProxyToConfig(proxy:ProxyServerModel) {
|
2018-10-20 14:27:39 +08:00
|
|
|
if let configDict = configs(from: [proxy]) {
|
|
|
|
self.saveToClashConfigFile(config: configDict)
|
|
|
|
NotificationCenter.default.post(Notification(name: kShouldUpDateConfig))
|
|
|
|
}
|
2018-10-07 21:03:16 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
extension ConfigFileFactory {
|
2018-10-14 22:48:51 +08:00
|
|
|
static func upgardeIniIfNeed() {
|
|
|
|
let iniPath = kConfigFolderPath + "config.ini"
|
|
|
|
guard FileManager.default.fileExists(atPath: iniPath) else {return}
|
2018-10-19 21:21:56 +08:00
|
|
|
guard !FileManager.default.fileExists(atPath: kConfigFilePath) else {return}
|
2018-10-14 22:48:51 +08:00
|
|
|
upgradeIni()
|
2018-10-19 13:44:37 +08:00
|
|
|
let targetPath = kConfigFolderPath + "config\(Date().timeIntervalSince1970).bak"
|
|
|
|
try? FileManager.default.moveItem(atPath: iniPath, toPath: targetPath)
|
2018-10-14 22:48:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
private static func upgradeIni() {
|
2018-10-07 21:03:16 +08:00
|
|
|
|
|
|
|
func parseOptions(options:[String]) -> [String:String] {
|
|
|
|
var mapping = [String:String]()
|
|
|
|
for option in options {
|
|
|
|
let pairs = option.split(separator: "=",maxSplits: 2)
|
|
|
|
guard pairs.count == 2 else {continue}
|
|
|
|
mapping[String(pairs[0]).trimed()] = String(pairs[1]).trimed()
|
|
|
|
}
|
|
|
|
return mapping
|
2018-08-26 13:25:29 +08:00
|
|
|
}
|
|
|
|
|
2018-10-14 23:42:53 +08:00
|
|
|
guard let ini = parseConfig("\(kConfigFolderPath)config.ini") else {
|
2018-08-26 13:25:29 +08:00
|
|
|
return
|
|
|
|
}
|
2018-10-07 21:03:16 +08:00
|
|
|
var newConfig = [String:Any]()
|
|
|
|
|
|
|
|
newConfig.merge(ini["General"]?.dict as [String : Any]? ?? [:]) { $1 }
|
2018-08-26 13:25:29 +08:00
|
|
|
|
2018-10-07 21:03:16 +08:00
|
|
|
var newProxies = [Any]()
|
|
|
|
|
|
|
|
for (proxy,elemsStr) in ini["Proxy"]?.dict ?? [:] {
|
|
|
|
|
|
|
|
let elems = elemsStr.split(separator: ",").map { (substring) -> String in
|
|
|
|
return String(substring).trimed()
|
|
|
|
}
|
|
|
|
|
|
|
|
if elems.count < 3 {continue}
|
|
|
|
|
|
|
|
let proxyName = proxy
|
|
|
|
let proxyType = elems[0]
|
|
|
|
let proxyAddr = elems[1]
|
|
|
|
guard let proxyPort = Int(elems[2]) else {continue}
|
2018-10-14 23:42:53 +08:00
|
|
|
var newProxy:[String : Any] = ["name":proxyName,
|
|
|
|
"server":proxyAddr,
|
|
|
|
"port":proxyPort,
|
|
|
|
"type":proxyType]
|
2018-10-07 21:03:16 +08:00
|
|
|
|
|
|
|
switch proxyType {
|
|
|
|
case "ss":
|
|
|
|
if elems.count < 5 {continue}
|
|
|
|
let otherOptions = parseOptions(options:Array(elems[5..<elems.count]))
|
|
|
|
print(otherOptions)
|
|
|
|
newProxy["cipher"] = elems[3]
|
|
|
|
newProxy["password"] = elems[4]
|
|
|
|
newProxy.merge(otherOptions) { $1 }
|
|
|
|
|
2018-10-14 23:42:53 +08:00
|
|
|
case "vmess":
|
2018-10-07 21:03:16 +08:00
|
|
|
if elems.count < 6 {continue}
|
|
|
|
newProxy["uuid"] = elems[3]
|
|
|
|
guard let alertId = Int(elems[4]) else {continue}
|
|
|
|
newProxy["alterId"] = alertId
|
|
|
|
newProxy["cipher"] = elems[5]
|
|
|
|
let otherOptions = parseOptions(options: Array(elems[6..<elems.count]))
|
|
|
|
newProxy.merge(otherOptions) { $1 }
|
|
|
|
case "socks":
|
|
|
|
if elems.count < 3 {continue}
|
|
|
|
|
|
|
|
default:
|
|
|
|
continue
|
|
|
|
|
2018-08-26 13:25:29 +08:00
|
|
|
}
|
2018-10-07 21:03:16 +08:00
|
|
|
newProxies.append(newProxy)
|
|
|
|
|
2018-08-26 13:25:29 +08:00
|
|
|
}
|
|
|
|
|
2018-10-07 21:03:16 +08:00
|
|
|
var newProxyGroup = [Any]()
|
2018-10-14 23:42:53 +08:00
|
|
|
for (group,groupStr) in ini["Proxy Group"]?.dictArray ?? [] {
|
2018-10-07 21:03:16 +08:00
|
|
|
var elems = groupStr.split(separator: ",").map { (substring) -> String in
|
|
|
|
return String(substring).trimed()
|
|
|
|
}
|
|
|
|
if elems.count<2 {continue}
|
|
|
|
let groupType = String(elems[0])
|
|
|
|
var proxyGroup = ["name":group,"type":groupType] as [String:Any]
|
|
|
|
|
|
|
|
switch groupType {
|
|
|
|
case "select":
|
|
|
|
let proxyNames = Array(elems.dropFirst())
|
|
|
|
proxyGroup["proxies"] = proxyNames
|
|
|
|
case "url-test","fallback":
|
2018-10-14 23:42:53 +08:00
|
|
|
proxyGroup["proxies"] = Array(elems.dropLast(2).dropFirst())
|
2018-10-07 21:03:16 +08:00
|
|
|
proxyGroup["url"] = elems[elems.count-2]
|
|
|
|
guard let delay = Int(elems[elems.count-1]) else {continue}
|
2018-10-14 23:42:53 +08:00
|
|
|
proxyGroup["interval"] = delay
|
2018-10-07 21:03:16 +08:00
|
|
|
default:
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
newProxyGroup.append(proxyGroup)
|
2018-08-26 13:25:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-10-07 21:03:16 +08:00
|
|
|
newConfig["Proxy"] = newProxies
|
|
|
|
newConfig["Proxy Group"] = newProxyGroup
|
|
|
|
newConfig["Rule"] = (ini["Rule"]?.array ?? [])
|
2018-10-14 23:42:53 +08:00
|
|
|
saveToClashConfigFile(config: newConfig)
|
2018-10-19 13:44:37 +08:00
|
|
|
showIniUpgradeAlert()
|
2018-08-26 13:25:29 +08:00
|
|
|
}
|
2018-10-07 21:03:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
extension ConfigFileFactory {
|
2018-09-05 19:30:38 +08:00
|
|
|
static func showReplacingConfigFileAlert() -> Bool{
|
|
|
|
let alert = NSAlert()
|
|
|
|
alert.messageText = """
|
2018-10-02 12:20:18 +08:00
|
|
|
Can't recognize your config file. We will backup and replace your config file in your config folder.
|
2018-09-05 19:30:38 +08:00
|
|
|
|
|
|
|
Otherwise the functions of ClashX will not work properly. You may need to restart ClashX or reload Config manually.
|
|
|
|
"""
|
|
|
|
alert.alertStyle = .warning
|
|
|
|
alert.addButton(withTitle: "Replace")
|
|
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
|
return alert.runModal() == .alertFirstButtonReturn
|
|
|
|
}
|
2018-10-07 21:03:16 +08:00
|
|
|
|
2018-10-19 13:44:37 +08:00
|
|
|
static func showIniUpgradeAlert() {
|
|
|
|
let alert = NSAlert()
|
|
|
|
alert.messageText = """
|
|
|
|
Clash has changed config file format from .ini to .yml.
|
|
|
|
ClashX has automatically upgraded your config file.
|
|
|
|
Note: current upgradation might cause your config file looks confusion. Check the config file example in github for better customize.
|
|
|
|
""".localized()
|
|
|
|
alert.alertStyle = .warning
|
|
|
|
alert.addButton(withTitle: "OK")
|
|
|
|
alert.runModal()
|
|
|
|
}
|
|
|
|
|
2018-08-05 18:34:12 +08:00
|
|
|
}
|