import Foundation /// Where ipx is. Asked for on first run and kept, because there is more than one way in: the /// Cloudflare tunnel from anywhere, and a LAN address at home. Nothing here is a secret -- /// signing in happens in the web view, and the cookies it leaves behind authenticate the rest. enum ServerSettings { private static let key = "ipx.server" /// What the setup screen starts with. A suggestion, not a default: nothing is stored until /// it is saved, so the first run always asks. static let suggestion = "https://ipodderx.sdf1.net" static var base: URL? { get { UserDefaults.standard.string(forKey: key).flatMap(parse) } set { UserDefaults.standard.set(newValue?.absoluteString, forKey: key) } } static var isConfigured: Bool { base != nil } /// What someone typed, as an address, or nil if it cannot be one. /// /// A bare host gets https, because typing `ipodderx.sdf1.net` is the common case and /// `URL(string:)` would otherwise hand back something with no scheme that fails much later, /// in a network error nobody can act on. static func parse(_ raw: String) -> URL? { var text = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return nil } if !text.contains("://") { text = "https://" + text } guard let url = URL(string: text), let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https", let host = url.host, !host.isEmpty else { return nil } return url } /// Whether plain HTTP will actually be allowed to load. The app permits it on a local /// network and nowhere else, so this is the difference between a clear warning at setup and /// a blocked request much later. static func isLocal(_ url: URL) -> Bool { guard let host = url.host?.lowercased() else { return false } if host == "localhost" || host.hasSuffix(".local") || host == "127.0.0.1" || host == "::1" { return true } if host.hasPrefix("192.168.") || host.hasPrefix("10.") || host.hasPrefix("169.254.") { return true } // 172.16.0.0/12 is the one that needs arithmetic rather than a prefix. let parts = host.split(separator: ".") if parts.count == 4, parts[0] == "172", let second = Int(parts[1]), (16...31).contains(second) { return true } return false } /// A path the page gave us, against the server. The page speaks in absolute paths /// (`/media/42`), which are relative to whichever host it was served from. static func url(_ path: String) -> URL? { if let u = URL(string: path), u.scheme != nil { return u } guard let base else { return nil } return URL(string: path, relativeTo: base)?.absoluteURL } }