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>
This commit is contained in:
@@ -4,24 +4,17 @@ import Security
|
||||
/// What the player and CarPlay send instead of the web view's cookie (#419).
|
||||
///
|
||||
/// The cookie Cloudflare Access leaves in the web view lasts as long as its session, and when it
|
||||
/// ends mid-drive the player has nothing. Two things that do not end by themselves replace it:
|
||||
/// an ipx API token (Settings, API tokens), which ipx reads as `Authorization: Bearer`, and an
|
||||
/// Access service token (Zero Trust, Service Auth), which Cloudflare reads as
|
||||
/// `CF-Access-Client-Id` and `CF-Access-Client-Secret` before the request reaches ipx. Either
|
||||
/// half alone is refused somewhere, so `headers` is empty unless it has all three, and the player
|
||||
/// then falls back to the cookie as before.
|
||||
/// ends mid-drive the player has nothing. An ipx API token (Settings, API tokens), which ipx reads
|
||||
/// as `Authorization: Bearer`, does not end by itself. Access would turn a request carrying only
|
||||
/// that away, so the player sends it to a second address with no Access in front, which
|
||||
/// Cloudflare's firewall holds to the paths the player uses (`Server.apiURL`).
|
||||
enum Credentials {
|
||||
enum Field: String, CaseIterable {
|
||||
case apiToken = "ipx.api-token"
|
||||
case accessId = "ipx.access-client-id"
|
||||
case accessSecret = "ipx.access-client-secret"
|
||||
}
|
||||
|
||||
private static let service = "net.sdf1.ipx"
|
||||
private static let account = "ipx.api-token"
|
||||
|
||||
static subscript(field: Field) -> String? {
|
||||
static var apiToken: String? {
|
||||
get {
|
||||
var query = base(field)
|
||||
var query = base
|
||||
query[kSecReturnData as String] = true
|
||||
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||
var out: CFTypeRef?
|
||||
@@ -30,10 +23,10 @@ enum Credentials {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
set {
|
||||
SecItemDelete(base(field) as CFDictionary)
|
||||
SecItemDelete(base as CFDictionary)
|
||||
guard let value = newValue?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!value.isEmpty else { return }
|
||||
var item = base(field)
|
||||
var item = base
|
||||
item[kSecValueData as String] = Data(value.utf8)
|
||||
// After first unlock, not when unlocked: CarPlay and a locked phone both play.
|
||||
item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
@@ -41,19 +34,17 @@ enum Credentials {
|
||||
}
|
||||
}
|
||||
|
||||
private static func base(_ field: Field) -> [String: Any] {
|
||||
private static var base: [String: Any] {
|
||||
[kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccount as String: field.rawValue]
|
||||
kSecAttrAccount as String: account]
|
||||
}
|
||||
|
||||
/// The headers for a request to the server, or none when any of the three is missing.
|
||||
/// Empty unless there is a token, and an address to send it to: sent to the page's own
|
||||
/// address it would only be turned away by Access.
|
||||
static var headers: [String: String] {
|
||||
guard let token = self[.apiToken], let id = self[.accessId],
|
||||
let secret = self[.accessSecret] else { return [:] }
|
||||
return ["Authorization": "Bearer \(token)",
|
||||
"CF-Access-Client-Id": id,
|
||||
"CF-Access-Client-Secret": secret]
|
||||
guard let token = apiToken, Server.apiURL != nil else { return [:] }
|
||||
return ["Authorization": "Bearer \(token)"]
|
||||
}
|
||||
|
||||
static var isSet: Bool { !headers.isEmpty }
|
||||
|
||||
Reference in New Issue
Block a user