ClashX.Meta/ClashX/General/ConfigFileFactory.swift

357 lines
13 KiB
Swift
Raw Normal View History

//
// ConfigFileFactory.swift
// ClashX
//
// Created by CYC on 2018/8/5.
2018-08-08 13:47:38 +08:00
// Copyright © 2018 yichengchen. All rights reserved.
//
import Foundation
import AppKit
import SwiftyJSON
2018-10-07 21:03:16 +08:00
import Yams
class ConfigFileFactory {
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")
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-10-14 22:48:51 +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-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)
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-27 23:07:05 +08:00
@discardableResult
static func backupAndRemoveConfigFile(showAlert:Bool = false) -> Bool {
let path = kConfigFilePath
2018-10-14 23:42:53 +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"
try? FileManager.default.moveItem(atPath: path, toPath: newPath)
}
return true
}
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 {
if (!backupAndRemoveConfigFile(showAlert: true)) {
return false
}
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)
NSUserNotificationCenter.default.postGenerateSimpleConfigNotice()
return true
}
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()
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
profile.pluginStr = item["plugin_opts"].stringValue
if remarkSet.contains(profile.remark) {
profile.remark.append("Dup")
}
if (profile.isValid()) {
profiles.append(profile)
}
}
}
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))
}
} else {
NSUserNotificationCenter
.default
.post(title: "Import Server Profile Fail!",
info: "No proxies are imported")
}
}
}
}
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}
guard !FileManager.default.fileExists(atPath: kConfigFilePath) else {return}
2018-10-14 22:48:51 +08:00
upgradeIni()
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-10-14 23:42:53 +08:00
guard let ini = parseConfig("\(kConfigFolderPath)config.ini") else {
return
}
2018-10-07 21:03:16 +08:00
var newConfig = [String:Any]()
newConfig.merge(ini["General"]?.dict as [String : Any]? ?? [:]) { $1 }
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-10-07 21:03:16 +08:00
newProxies.append(newProxy)
}
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-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)
showIniUpgradeAlert()
}
2018-10-07 21:03:16 +08:00
}
extension ConfigFileFactory {
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.
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
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()
}
}