import Foundation /// Which ipx-web the app shows. Asked for on first launch; shaking the phone asks again. /// `-ipx.server ` on the launch arguments overrides it for that run (UserDefaults' argument /// domain), which is how a test points the app somewhere else. enum Server { private static let key = "ipx.server" static let suggested = "https://ipodderx.sdf1.net" static var url: URL? { get { UserDefaults.standard.string(forKey: key).flatMap(normalise) } set { UserDefaults.standard.set(newValue?.absoluteString, forKey: key) } } /// What someone typed, as an address: a bare host gets https. Plain http is allowed only on /// your own network, which is all NSAllowsLocalNetworking lets through, so it is refused here /// rather than failing later and looking like the server is down. static func normalise(_ text: String) -> URL? { var s = text.trimmingCharacters(in: .whitespacesAndNewlines) if s.isEmpty { return nil } if !s.contains("://") { s = "https://" + s } guard let u = URL(string: s), let host = u.host, !host.isEmpty else { return nil } switch u.scheme { case "https": return u case "http": return isLocal(host) ? u : nil default: return nil } } static func isLocal(_ host: String) -> Bool { if host == "localhost" || host.hasSuffix(".local") { return true } let p = host.split(separator: ".").compactMap { Int($0) } guard p.count == 4 else { return false } return p[0] == 10 || p[0] == 127 || (p[0] == 192 && p[1] == 168) || (p[0] == 172 && (16...31).contains(p[1])) } /// The hosts in the Info.plist's WKAppBoundDomains (#435): the server and where its sign-in /// goes. Fixed when the app is built, as Apple has it; a debug build adds localhost. static let appBoundHosts = Set(Bundle.main.object(forInfoDictionaryKey: "WKAppBoundDomains") as? [String] ?? []) static func isAppBound(_ url: URL?) -> Bool { url?.host.map { appBoundHosts.contains($0.lowercased()) } ?? false } /// A path the page sent (`/media/42`), against the server. static func resolve(_ path: String) -> URL? { guard let base = url else { return nil } return URL(string: path, relativeTo: base)?.absoluteURL } /// Where the player, the badge and CarPlay talk to ipx, which is not where the page is: an /// address with no Cloudflare Access in front, for the API token alone (#419). Always https, /// since the token travels in a header. private static let apiKey = "ipx.api" static let suggestedAPI = "https://ipxapi.sdf1.net" static var apiURL: URL? { get { UserDefaults.standard.string(forKey: apiKey).flatMap(normalise).flatMap { $0.scheme == "https" ? $0 : nil } } set { UserDefaults.standard.set(newValue?.absoluteString, forKey: apiKey) } } /// A path against the player's address when there are credentials to send there, else the /// page's, where the web view's cookie does the signing in. static func resolveForPlayer(_ path: String) -> URL? { guard Credentials.isSet, let base = apiURL else { return resolve(path) } return URL(string: path, relativeTo: base)?.absoluteURL } }