mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 02:04:13 +00:00
177 lines
6.7 KiB
Swift
177 lines
6.7 KiB
Swift
import Foundation
|
||
|
||
/// #33 偏好存储。非敏感字段写入受保护的 JSON。GFWList URL 与 PAC 用户规则
|
||
/// 已随 issue #67 移除;未发布的 NG2 PAC 偏好不做迁移。
|
||
struct ProxySettingsFileStore: ProxySettingsStoring {
|
||
let fileURL: URL
|
||
let legacyListenFileURL: URL
|
||
|
||
init(
|
||
fileURL: URL = ProxySettingsFileStore.defaultFileURL(),
|
||
legacyListenFileURL: URL = ListenSettingsFileStore.defaultFileURL()
|
||
) {
|
||
self.fileURL = fileURL
|
||
self.legacyListenFileURL = legacyListenFileURL
|
||
}
|
||
|
||
static func defaultFileURL() -> URL {
|
||
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[
|
||
0]
|
||
return support.appendingPathComponent("ShadowsocksX-NG2/settings.json")
|
||
}
|
||
|
||
func load() throws -> ProxySettings {
|
||
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||
do {
|
||
let listen = try ListenSettingsFileStore(fileURL: legacyListenFileURL).load()
|
||
return try validated(ProxySettings(listen: listen))
|
||
} catch let error as ListenSettingsStoreError {
|
||
throw ProxySettingsStoreError.legacyListenSettings(error)
|
||
} catch let error as ProxySettingsStoreError {
|
||
throw error
|
||
} catch {
|
||
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
|
||
}
|
||
}
|
||
|
||
let data: Data
|
||
do {
|
||
data = try Data(contentsOf: fileURL)
|
||
} catch {
|
||
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
|
||
}
|
||
let record: ProxySettingsRecord
|
||
do {
|
||
record = try JSONDecoder().decode(ProxySettingsRecord.self, from: data)
|
||
} catch {
|
||
throw ProxySettingsStoreError.corrupt(detail: String(describing: error))
|
||
}
|
||
return try settings(from: record)
|
||
}
|
||
|
||
func save(_ settings: ProxySettings) throws {
|
||
_ = try validated(settings)
|
||
do {
|
||
let data = try Self.jsonEncoder.encode(record(from: settings))
|
||
try AtomicFileWriter.write(data, to: fileURL)
|
||
} catch let error as ProxySettingsStoreError {
|
||
throw error
|
||
} catch let error as AtomicFileWriter.WriteError {
|
||
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
|
||
} catch {
|
||
throw ProxySettingsStoreError.ioFailure(detail: String(describing: error))
|
||
}
|
||
}
|
||
|
||
static func restored(store: ProxySettingsFileStore = ProxySettingsFileStore())
|
||
-> RestoredProxySettings
|
||
{
|
||
do {
|
||
return RestoredProxySettings(settings: try store.load(), unreadableError: nil)
|
||
} catch let error as ProxySettingsStoreError {
|
||
RuntimeLog.emit(.listenSettingsUnreadable(detail: String(describing: error)))
|
||
return RestoredProxySettings(settings: ProxySettings(), unreadableError: error)
|
||
} catch {
|
||
let wrapped = ProxySettingsStoreError.ioFailure(detail: String(describing: error))
|
||
RuntimeLog.emit(.listenSettingsUnreadable(detail: String(describing: error)))
|
||
return RestoredProxySettings(settings: ProxySettings(), unreadableError: wrapped)
|
||
}
|
||
}
|
||
|
||
private func validated(_ settings: ProxySettings) throws -> ProxySettings {
|
||
let errors = settings.validationErrors
|
||
guard errors.isEmpty else { throw ProxySettingsStoreError.invalid(errors) }
|
||
return settings
|
||
}
|
||
|
||
private func settings(from record: ProxySettingsRecord) throws -> ProxySettings {
|
||
var listen = SslocalListenSettings()
|
||
listen.listenerMode = record.listenerMode
|
||
listen.socksPort = record.socksPort
|
||
listen.httpPort = record.httpPort
|
||
|
||
return try validated(
|
||
ProxySettings(
|
||
listen: listen,
|
||
proxyExceptions: record.proxyExceptions,
|
||
preferredMode: record.preferredMode,
|
||
ruleDefaultAction: record.ruleDefaultAction,
|
||
agentEnabled: record.agentEnabled,
|
||
systemProxyEnabled: record.systemProxyEnabled))
|
||
}
|
||
|
||
private func record(from settings: ProxySettings) -> ProxySettingsRecord {
|
||
var record = ProxySettingsRecord()
|
||
record.listenerMode = settings.listen.listenerMode
|
||
record.socksPort = settings.listen.socksPort
|
||
record.httpPort = settings.listen.httpPort
|
||
record.proxyExceptions = settings.proxyExceptions
|
||
record.preferredMode = settings.preferredMode
|
||
record.ruleDefaultAction = settings.ruleDefaultAction
|
||
record.agentEnabled = settings.agentEnabled
|
||
record.systemProxyEnabled = settings.systemProxyEnabled
|
||
return record
|
||
}
|
||
|
||
private static let jsonEncoder: JSONEncoder = {
|
||
let encoder = JSONEncoder()
|
||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||
return encoder
|
||
}()
|
||
}
|
||
|
||
private struct ProxySettingsRecord: Codable, Equatable, Sendable {
|
||
var listenerMode: ListenerMode = .localhost
|
||
var socksPort: Int = SslocalListenSettings.defaultSocksPort
|
||
var httpPort: Int = SslocalListenSettings.defaultHTTPPort
|
||
var proxyExceptions: String = ProxySettings.defaultProxyExceptions
|
||
var preferredMode: ProxyModeKind = .rule
|
||
var ruleDefaultAction: RuleDefaultAction = .proxyWhenUnmatched
|
||
var agentEnabled: Bool = false
|
||
var systemProxyEnabled: Bool = false
|
||
|
||
init() {}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
listenerMode =
|
||
try container.decodeIfPresent(ListenerMode.self, forKey: .listenerMode) ?? .localhost
|
||
socksPort =
|
||
try container.decodeIfPresent(Int.self, forKey: .socksPort)
|
||
?? SslocalListenSettings.defaultSocksPort
|
||
httpPort =
|
||
try container.decodeIfPresent(Int.self, forKey: .httpPort)
|
||
?? SslocalListenSettings.defaultHTTPPort
|
||
proxyExceptions =
|
||
try container.decodeIfPresent(String.self, forKey: .proxyExceptions)
|
||
?? ProxySettings.defaultProxyExceptions
|
||
// 旧 NG2 偏好里的 "pac" 等已删除值容错回落到规则模式(issue #67:
|
||
// 未发布的 PAC 偏好不做迁移,但解码失败会把全部保留字段连坐丢掉)。
|
||
if let rawMode = try container.decodeIfPresent(String.self, forKey: .preferredMode),
|
||
let mode = ProxyModeKind(rawValue: rawMode)
|
||
{
|
||
preferredMode = mode
|
||
} else {
|
||
preferredMode = .rule
|
||
}
|
||
// 规则模式子选项(issue #63):出厂「未匹配时代理」。
|
||
ruleDefaultAction =
|
||
try container.decodeIfPresent(RuleDefaultAction.self, forKey: .ruleDefaultAction)
|
||
?? .proxyWhenUnmatched
|
||
// 未发版无迁移:缺省 agent off、系统代理 off(ADR-0011)。
|
||
agentEnabled = try container.decodeIfPresent(Bool.self, forKey: .agentEnabled) ?? false
|
||
systemProxyEnabled =
|
||
try container.decodeIfPresent(Bool.self, forKey: .systemProxyEnabled) ?? false
|
||
}
|
||
}
|
||
|
||
extension ProxySettingsRecord {
|
||
fileprivate enum CodingKeys: String, CodingKey {
|
||
case listenerMode, socksPort, httpPort
|
||
case proxyExceptions
|
||
case preferredMode
|
||
case ruleDefaultAction
|
||
case agentEnabled, systemProxyEnabled
|
||
}
|
||
}
|