Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/ProxySettingsFileStore.swift
T

177 lines
6.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}