mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
119 lines
5.6 KiB
Swift
119 lines
5.6 KiB
Swift
import Foundation
|
|
import Network
|
|
|
|
public struct ApplicationSettings: Codable, Equatable, Sendable {
|
|
public var userAgent = ""
|
|
public var proxy = ""
|
|
public var hosts = ""
|
|
public var timeoutSeconds = 0
|
|
public var concurrentSources = 32
|
|
public var chapterCacheMB = 256
|
|
public var prefetchChapters = 1
|
|
public var recordLog = false
|
|
public var ignoreCertificateErrors = true
|
|
|
|
public init() {}
|
|
|
|
public var effectiveUserAgent: String {
|
|
userAgent.isEmpty ? LegadoURLBuilder.defaultUserAgent : userAgent
|
|
}
|
|
|
|
public func validated() throws -> Self {
|
|
var value = self
|
|
value.userAgent = userAgent.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
value.proxy = proxy.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !value.userAgent.contains(where: { $0.isNewline || $0 == "\0" }) else {
|
|
throw SettingsError.invalid("User-Agent 不能包含换行。")
|
|
}
|
|
if !value.proxy.isEmpty { _ = try Self.proxyDictionary(value.proxy) }
|
|
_ = try Self.parseHosts(hosts)
|
|
guard timeoutSeconds == 0 || (5...300).contains(timeoutSeconds),
|
|
(1...64).contains(concurrentSources), (16...4096).contains(chapterCacheMB),
|
|
(0...20).contains(prefetchChapters) else {
|
|
throw SettingsError.invalid("网络或缓存设置超出允许范围。")
|
|
}
|
|
return value
|
|
}
|
|
|
|
public func configure(_ configuration: URLSessionConfiguration, proxyOverride: String? = nil) throws {
|
|
configuration.httpMaximumConnectionsPerHost = concurrentSources
|
|
let address = proxyOverride ?? proxy
|
|
configuration.connectionProxyDictionary = address.isEmpty ? nil : try Self.proxyDictionary(address)
|
|
if timeoutSeconds > 0 {
|
|
configuration.timeoutIntervalForRequest = Double(timeoutSeconds)
|
|
configuration.timeoutIntervalForResource = Double(timeoutSeconds)
|
|
}
|
|
}
|
|
|
|
public func configure(_ request: URLRequest) -> URLRequest {
|
|
var request = request
|
|
if timeoutSeconds > 0 { request.timeoutInterval = Double(timeoutSeconds) }
|
|
if request.value(forHTTPHeaderField: "User-Agent") == nil,
|
|
URLProtocol.property(forKey: "LegadoSuppressUserAgent", in: request) as? Bool != true {
|
|
request.setValue(effectiveUserAgent, forHTTPHeaderField: "User-Agent")
|
|
}
|
|
return request
|
|
}
|
|
|
|
public static func proxyDictionary(_ address: String) throws -> [AnyHashable: Any] {
|
|
guard let url = URL(string: address), let host = url.host, !host.isEmpty,
|
|
let scheme = url.scheme?.lowercased(), ["http", "https", "socks", "socks4", "socks5"].contains(scheme),
|
|
url.user == nil, url.password == nil, url.query == nil, url.fragment == nil,
|
|
url.path.isEmpty || url.path == "/", (1...65535).contains(url.port ?? 8080) else {
|
|
throw SettingsError.invalid("代理格式应为 http://主机:端口、https://主机:端口或 socks5://主机:端口,不含账号和密码。")
|
|
}
|
|
let port = url.port ?? (scheme.hasPrefix("socks") ? 1080 : scheme == "https" ? 443 : 8080)
|
|
if scheme.hasPrefix("socks") {
|
|
return ["SOCKSEnable": 1, "SOCKSProxy": host, "SOCKSPort": port]
|
|
}
|
|
return ["HTTPEnable": 1, "HTTPProxy": host, "HTTPPort": port,
|
|
"HTTPSEnable": 1, "HTTPSProxy": host, "HTTPSPort": port]
|
|
}
|
|
|
|
public static func parseHosts(_ text: String) throws -> [String: String] {
|
|
var values: [String: String] = [:]
|
|
for (index, line) in text.components(separatedBy: .newlines).enumerated() {
|
|
let fields = line.split(separator: "#", maxSplits: 1, omittingEmptySubsequences: false)[0]
|
|
.split(whereSeparator: { $0.isWhitespace }).map(String.init)
|
|
guard !fields.isEmpty else { continue }
|
|
guard fields.count >= 2, IPv4Address(fields[0]) != nil || IPv6Address(fields[0]) != nil else {
|
|
throw SettingsError.invalid("Hosts 第 \(index + 1) 行应为 IP地址 域名,可添加 # 注释。")
|
|
}
|
|
for domain in fields.dropFirst() {
|
|
let domain = domain.lowercased()
|
|
guard domain.utf8.count <= 253, domain.split(separator: ".", omittingEmptySubsequences: false).allSatisfy({ label in
|
|
!label.isEmpty && label.count <= 63 && label.first != "-" && label.last != "-"
|
|
&& label.utf8.allSatisfy { (97...122).contains($0) || (48...57).contains($0) || $0 == 45 }
|
|
}) else { throw SettingsError.invalid("Hosts 第 \(index + 1) 行的域名无效。") }
|
|
values[domain] = fields[0]
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
}
|
|
|
|
public enum SettingsError: Error, LocalizedError {
|
|
case invalid(String)
|
|
public var errorDescription: String? { if case .invalid(let message) = self { message } else { nil } }
|
|
}
|
|
|
|
public final class ApplicationSettingsStore: @unchecked Sendable {
|
|
public static let shared = ApplicationSettingsStore()
|
|
public static let key = "legado.applicationSettings.v1"
|
|
private let defaults: UserDefaults
|
|
|
|
public init(defaults: UserDefaults = .standard) { self.defaults = defaults }
|
|
|
|
public var settings: ApplicationSettings {
|
|
guard let data = defaults.data(forKey: Self.key),
|
|
let value = try? JSONDecoder().decode(ApplicationSettings.self, from: data),
|
|
let validated = try? value.validated() else { return .init() }
|
|
return validated
|
|
}
|
|
|
|
public func save(_ settings: ApplicationSettings) throws {
|
|
let validated = try settings.validated()
|
|
defaults.set(try JSONEncoder().encode(validated), forKey: Self.key)
|
|
}
|
|
}
|