A WKWebView runs no service worker unless the app lists its domains in WKAppBoundDomains and the web view limits itself to them. So the Info.plist lists ipodderx.sdf1.net, with Cloudflare Access's (rays-sdf1.cloudflareaccess.com) and Authentik's (authentik.sdf1.net) hosts so signing in still works. For a server in that list the web view is made app-bound and #389's worker runs: the page and its lists open offline from the second launch, since the first only installs the worker. The costs: - Navigation elsewhere is refused, and the failure looked like the server being down. decidePolicyFor sends such links to Safari. - With the key present, WebKit cuts the bridge for any server not in the list. That server then gets the page with its own audio, not the phone's player. The web view is made again when the server changes, since being app-bound is fixed when it is made. A debug build adds localhost through a build script, so the simulator can test against a page served on the Mac; the release archive's plist was checked to have only the three. Tested in the simulator: opened twice online, the server stopped, relaunched, and the page came up from the worker. With the bridge still up on the app-bound host, the phone's player played. 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; shaking the phone asks again.
|
|
/// `-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
|
|
}
|
|
}
|