The Access service token plus API token pair was three values to enter and Access in front of both. The player, the badge and CarPlay now send the ipx API token alone to https://ipxapi.sdf1.net: a route of the rays-unraid tunnel to the same ipx-web, with no Access application, held to the paths the app uses by a custom firewall rule on the sdf1.net zone (403 for anything without a bearer header or off the list: the page, the login form, /api/login, ?token= sign-in, the admin API). docs/sso.md says so, since it also says not to bypass Access for /api/*; this is that warning's narrow version, and the rule is on Cloudflare, not here. The unread badge (#421) shows All Subscriptions' count, as the page's badge does: set when the app comes forward, after an item is read, and by a background refresh iOS runs now and then. The test target had no Info.plist and could never have run; it does now. Build 4. Not yet run on a phone with the token set. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
62 lines
2.8 KiB
Swift
62 lines
2.8 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]))
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|