The server address and the player token were set from alerts that opened on shaking the phone, which nobody finds. Settings now has a This device section in the app: the server, the player token and its address, and a Make button that creates an API token and hands it straight to the app's keychain. The app sends what it has at ready (appSettings) and takes setServer, setToken and clearToken; a new server rebuilds the web view as the alert did. The first-launch prompt and the Cannot reach alert stay native, since there is no page then. The iPhone is portrait only; the iPad keeps all four orientations, which Split View and Slide Over need (#434 checked the iPad's layout). Build 14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
3.2 KiB
Swift
70 lines
3.2 KiB
Swift
import Foundation
|
|
|
|
/// Which ipx-web the app shows. Asked for on first launch; changed in the page's Settings (#445).
|
|
/// `-ipx.server <url>` 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
|
|
}
|
|
}
|