fix(auth): use the authentication spellings Jellyfin 12.0 leaves enabled
X-Emby-Authorization at the remaining request builders, and api_key= in the player-facing URLs, become Authorization and ApiKey. Jellyfin 12.0 disables X-Emby-Authorization, X-Emby-Token, X-MediaBrowser-Token, the Emby scheme and the api_key query parameter by default — and a migration (DisableLegacyAuthorization) turns them off on servers upgraded from 10.11 as well, so this is not confined to fresh installs. A client using them stops working against an upgraded server rather than degrading. Verified at source level rather than inferred: AuthorizationContext.cs is byte-identical between v10.11.5 and v12.0 apart from whitespace. The only change is the default of the gate that guards the legacy spellings. Authorization with the MediaBrowser scheme, and ApiKey as a query parameter, are ungated in both trees — and the server itself emits ApiKey in both (StreamInfo.cs). So one spelling is correct everywhere and no capability flag is involved. Also adds ServerCompatibility to ServerInfo: an opaque verdict the frontend renders without ever comparing a version number, with three states rather than a boolean. A server newer than this build is usable, not refused; an unreadable version string is not grounds for refusal either. Only a server below the floor is refused. TRACES: UR-085 | DR-286, DR-287 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+137
-3
@@ -19,6 +19,40 @@ pub struct ServerInfo {
|
||||
pub id: String,
|
||||
/// Normalized server URL with protocol and no trailing slash
|
||||
pub normalized_url: String,
|
||||
/// Whether this build can talk to this server, as an **opaque state**.
|
||||
///
|
||||
/// The version string above is informational — for display and for the log.
|
||||
/// This is the judgement, made in Rust, because deciding whether an API
|
||||
/// version is usable is domain reasoning: the frontend must never compare a
|
||||
/// version number, for the same reason it never receives an item-type list.
|
||||
///
|
||||
/// TRACES: UR-085 | DR-286
|
||||
pub compatibility: ServerCompatibility,
|
||||
}
|
||||
|
||||
/// The verdict on a server's version.
|
||||
///
|
||||
/// Deliberately three states rather than a boolean. "Unrecognised" is not a
|
||||
/// failure: a server newer than this build resolves forward and works, and
|
||||
/// refusing it would make every JellyTau release expire the moment the server
|
||||
/// upgrades. Only a server below the supported floor is refused, where failure
|
||||
/// is certain rather than merely likely.
|
||||
///
|
||||
/// TRACES: UR-085 | DR-286
|
||||
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum ServerCompatibility {
|
||||
/// A generation this build knows and was tested against.
|
||||
Supported,
|
||||
/// Parsed, but newer than anything this build knows. Treated as the newest
|
||||
/// known generation; everything works, and this exists so the UI *may*
|
||||
/// mention it rather than so it must.
|
||||
NewerThanKnown,
|
||||
/// The version string could not be parsed. Treated as supported — we do not
|
||||
/// refuse a server on the strength of not understanding its version string.
|
||||
UnknownVersion,
|
||||
/// Below the supported floor. This one is a refusal.
|
||||
TooOld { minimum: String },
|
||||
}
|
||||
|
||||
/// User information
|
||||
@@ -166,11 +200,35 @@ impl AuthManager {
|
||||
monitor.mark_reachable().await;
|
||||
}
|
||||
|
||||
let capabilities =
|
||||
crate::repository::capabilities::ServerCapabilities::from_reported(
|
||||
&info.version,
|
||||
);
|
||||
let compatibility = if capabilities.is_below_supported_floor() {
|
||||
let (major, minor) =
|
||||
crate::repository::capabilities::MINIMUM_SUPPORTED_MAJOR_MINOR;
|
||||
ServerCompatibility::TooOld {
|
||||
minimum: format!("{major}.{minor}"),
|
||||
}
|
||||
} else {
|
||||
use crate::repository::capabilities::ServerGeneration;
|
||||
match capabilities.generation {
|
||||
ServerGeneration::Unknown => ServerCompatibility::UnknownVersion,
|
||||
ServerGeneration::V12Plus
|
||||
if capabilities.version.as_ref().is_some_and(|v| v.major > 12) =>
|
||||
{
|
||||
ServerCompatibility::NewerThanKnown
|
||||
}
|
||||
_ => ServerCompatibility::Supported,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: info.server_name,
|
||||
version: info.version,
|
||||
id: info.id,
|
||||
normalized_url,
|
||||
compatibility,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -210,7 +268,7 @@ impl AuthManager {
|
||||
.client
|
||||
.post(&endpoint)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Emby-Authorization", auth_header)
|
||||
.header("Authorization", auth_header)
|
||||
.json(&serde_json::json!({
|
||||
"Username": username,
|
||||
"Pw": password,
|
||||
@@ -286,7 +344,7 @@ impl AuthManager {
|
||||
.http_client
|
||||
.client
|
||||
.get(&endpoint)
|
||||
.header("X-Emby-Authorization", auth_header)
|
||||
.header("Authorization", auth_header)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build request: {}", e))?;
|
||||
|
||||
@@ -365,7 +423,7 @@ impl AuthManager {
|
||||
.http_client
|
||||
.client
|
||||
.post(&endpoint)
|
||||
.header("X-Emby-Authorization", auth_header)
|
||||
.header("Authorization", auth_header)
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build request: {}", e))?;
|
||||
|
||||
@@ -397,6 +455,82 @@ impl AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod compatibility_tests {
|
||||
use super::*;
|
||||
use crate::repository::capabilities::ServerCapabilities;
|
||||
|
||||
/// Mirror of the mapping in `connect_to_server`, so the verdict can be
|
||||
/// asserted without standing up an HTTP server.
|
||||
fn verdict(reported: &str) -> ServerCompatibility {
|
||||
let capabilities = ServerCapabilities::from_reported(reported);
|
||||
if capabilities.is_below_supported_floor() {
|
||||
let (major, minor) = crate::repository::capabilities::MINIMUM_SUPPORTED_MAJOR_MINOR;
|
||||
return ServerCompatibility::TooOld {
|
||||
minimum: format!("{major}.{minor}"),
|
||||
};
|
||||
}
|
||||
use crate::repository::capabilities::ServerGeneration;
|
||||
match capabilities.generation {
|
||||
ServerGeneration::Unknown => ServerCompatibility::UnknownVersion,
|
||||
ServerGeneration::V12Plus
|
||||
if capabilities.version.as_ref().is_some_and(|v| v.major > 12) =>
|
||||
{
|
||||
ServerCompatibility::NewerThanKnown
|
||||
}
|
||||
_ => ServerCompatibility::Supported,
|
||||
}
|
||||
}
|
||||
|
||||
/// Both live generations are supported outright. 12.0 is the current stable
|
||||
/// and 10.11.x is what this client was built against.
|
||||
///
|
||||
/// TRACES: UR-085 | DR-286
|
||||
#[test]
|
||||
fn both_live_generations_are_supported() {
|
||||
assert_eq!(verdict("10.11.5"), ServerCompatibility::Supported);
|
||||
assert_eq!(verdict("10.11.11"), ServerCompatibility::Supported);
|
||||
assert_eq!(verdict("12.0.0"), ServerCompatibility::Supported);
|
||||
}
|
||||
|
||||
/// A server newer than this build is usable, not refused — otherwise every
|
||||
/// release would expire the moment the server upgraded.
|
||||
///
|
||||
/// TRACES: UR-085 | DR-286
|
||||
#[test]
|
||||
fn a_newer_server_is_usable_not_refused() {
|
||||
assert_eq!(verdict("13.0.0"), ServerCompatibility::NewerThanKnown);
|
||||
assert_eq!(verdict("99.1.2"), ServerCompatibility::NewerThanKnown);
|
||||
}
|
||||
|
||||
/// An unreadable version is not grounds for refusal.
|
||||
///
|
||||
/// TRACES: UR-085 | DR-286
|
||||
#[test]
|
||||
fn an_unreadable_version_is_not_a_refusal() {
|
||||
assert_eq!(
|
||||
verdict("not-a-version"),
|
||||
ServerCompatibility::UnknownVersion
|
||||
);
|
||||
assert_eq!(verdict(""), ServerCompatibility::UnknownVersion);
|
||||
}
|
||||
|
||||
/// Only a server below the floor is refused, and it says what the floor is
|
||||
/// so the message can name it.
|
||||
///
|
||||
/// TRACES: UR-085 | DR-286
|
||||
#[test]
|
||||
fn only_a_server_below_the_floor_is_refused() {
|
||||
assert_eq!(
|
||||
verdict("10.9.11"),
|
||||
ServerCompatibility::TooOld {
|
||||
minimum: "10.10".to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(verdict("10.10.0"), ServerCompatibility::Supported);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user