use anyhow::{anyhow, Result}; use pbkdf2::{ password_hash::{PasswordHash, PasswordVerifier}, Pbkdf2, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] pub struct AuthRequest { #[serde(rename = "Username")] pub username: String, #[serde(rename = "Pw")] pub pw: String, } #[derive(Debug, Serialize)] pub struct AuthResponse { #[serde(rename = "User")] pub user: AuthUser, #[serde(rename = "SessionInfo")] pub session_info: SessionInfo, #[serde(rename = "AccessToken")] pub access_token: String, #[serde(rename = "ServerId")] pub server_id: String, } #[derive(Debug, Serialize)] pub struct AuthUser { #[serde(rename = "Name")] pub name: String, #[serde(rename = "ServerId")] pub server_id: String, #[serde(rename = "Id")] pub id: String, #[serde(rename = "HasPassword")] pub has_password: bool, #[serde(rename = "HasConfiguredPassword")] pub has_configured_password: bool, #[serde(rename = "HasConfiguredEasyPassword")] pub has_configured_easy_password: bool, #[serde(rename = "EnableAutoLogin")] pub enable_auto_login: bool, #[serde(rename = "LastLoginDate")] pub last_login_date: Option, #[serde(rename = "LastActivityDate")] pub last_activity_date: Option, #[serde(rename = "Configuration")] pub configuration: UserConfiguration, #[serde(rename = "Policy")] pub policy: UserPolicy, } #[derive(Debug, Serialize)] pub struct UserConfiguration { #[serde(rename = "PlayDefaultAudioTrack")] pub play_default_audio_track: bool, #[serde(rename = "SubtitleLanguagePreference")] pub subtitle_language_preference: String, #[serde(rename = "DisplayMissingEpisodes")] pub display_missing_episodes: bool, #[serde(rename = "GroupedFolders")] pub grouped_folders: Vec, #[serde(rename = "SubtitleMode")] pub subtitle_mode: String, #[serde(rename = "DisplayCollectionsView")] pub display_collections_view: bool, #[serde(rename = "EnableLocalPassword")] pub enable_local_password: bool, #[serde(rename = "OrderedViews")] pub ordered_views: Vec, #[serde(rename = "LatestItemsExcludes")] pub latest_items_excludes: Vec, #[serde(rename = "MyMediaExcludes")] pub my_media_excludes: Vec, #[serde(rename = "HidePlayedInLatest")] pub hide_played_in_latest: bool, #[serde(rename = "RememberAudioSelections")] pub remember_audio_selections: bool, #[serde(rename = "RememberSubtitleSelections")] pub remember_subtitle_selections: bool, #[serde(rename = "EnableNextEpisodeAutoPlay")] pub enable_next_episode_auto_play: bool, #[serde(rename = "CastReceiverId")] pub cast_receiver_id: Option, } #[derive(Debug, Serialize)] pub struct UserPolicy { #[serde(rename = "IsAdministrator")] pub is_administrator: bool, #[serde(rename = "IsHidden")] pub is_hidden: bool, #[serde(rename = "EnableCollectionManagement")] pub enable_collection_management: bool, #[serde(rename = "EnableSubtitleManagement")] pub enable_subtitle_management: bool, #[serde(rename = "EnableLyricManagement")] pub enable_lyric_management: bool, #[serde(rename = "IsDisabled")] pub is_disabled: bool, #[serde(rename = "MaxParentalRating")] pub max_parental_rating: Option, #[serde(rename = "BlockedTags")] pub blocked_tags: Vec, #[serde(rename = "AllowedTags")] pub allowed_tags: Vec, #[serde(rename = "EnableUserPreferenceAccess")] pub enable_user_preference_access: bool, #[serde(rename = "AccessSchedules")] pub access_schedules: Vec, #[serde(rename = "BlockUnratedItems")] pub block_unrated_items: Vec, #[serde(rename = "EnableRemoteControlOfOtherUsers")] pub enable_remote_control_of_other_users: bool, #[serde(rename = "EnableSharedDeviceControl")] pub enable_shared_device_control: bool, #[serde(rename = "EnableRemoteAccess")] pub enable_remote_access: bool, #[serde(rename = "EnableLiveTvManagement")] pub enable_live_tv_management: bool, #[serde(rename = "EnableLiveTvAccess")] pub enable_live_tv_access: bool, #[serde(rename = "EnableMediaPlayback")] pub enable_media_playback: bool, #[serde(rename = "EnableAudioPlaybackTranscoding")] pub enable_audio_playback_transcoding: bool, #[serde(rename = "EnableVideoPlaybackTranscoding")] pub enable_video_playback_transcoding: bool, #[serde(rename = "EnablePlaybackRemuxing")] pub enable_playback_remuxing: bool, #[serde(rename = "ForceRemoteSourceTranscoding")] pub force_remote_source_transcoding: bool, #[serde(rename = "EnableContentDeletion")] pub enable_content_deletion: bool, #[serde(rename = "EnableContentDeletionFromFolders")] pub enable_content_deletion_from_folders: Vec, #[serde(rename = "EnableContentDownloading")] pub enable_content_downloading: bool, #[serde(rename = "EnableSyncTranscoding")] pub enable_sync_transcoding: bool, #[serde(rename = "EnableMediaConversion")] pub enable_media_conversion: bool, #[serde(rename = "EnabledDevices")] pub enabled_devices: Vec, #[serde(rename = "EnableAllDevices")] pub enable_all_devices: bool, #[serde(rename = "EnabledChannels")] pub enabled_channels: Vec, #[serde(rename = "EnableAllChannels")] pub enable_all_channels: bool, #[serde(rename = "EnabledFolders")] pub enabled_folders: Vec, #[serde(rename = "EnableAllFolders")] pub enable_all_folders: bool, #[serde(rename = "InvalidLoginAttemptCount")] pub invalid_login_attempt_count: i32, #[serde(rename = "LoginAttemptsBeforeLockout")] pub login_attempts_before_lockout: i32, #[serde(rename = "MaxActiveSessions")] pub max_active_sessions: i32, #[serde(rename = "EnablePublicSharing")] pub enable_public_sharing: bool, #[serde(rename = "BlockedMediaFolders")] pub blocked_media_folders: Vec, #[serde(rename = "BlockedChannels")] pub blocked_channels: Vec, #[serde(rename = "RemoteClientBitrateLimit")] pub remote_client_bitrate_limit: i32, #[serde(rename = "AuthenticationProviderId")] pub authentication_provider_id: String, #[serde(rename = "PasswordResetProviderId")] pub password_reset_provider_id: String, #[serde(rename = "SyncPlayAccess")] pub sync_play_access: String, } #[derive(Debug, Serialize)] pub struct SessionInfo { #[serde(rename = "PlayState")] pub play_state: PlayState, #[serde(rename = "AdditionalUsers")] pub additional_users: Vec, #[serde(rename = "Capabilities")] pub capabilities: Capabilities, #[serde(rename = "RemoteEndPoint")] pub remote_end_point: String, #[serde(rename = "Id")] pub id: String, #[serde(rename = "UserId")] pub user_id: String, #[serde(rename = "UserName")] pub user_name: String, #[serde(rename = "Client")] pub client: String, #[serde(rename = "LastActivityDate")] pub last_activity_date: String, #[serde(rename = "LastPlaybackCheckIn")] pub last_playback_check_in: String, #[serde(rename = "DeviceName")] pub device_name: String, #[serde(rename = "DeviceType")] pub device_type: String, #[serde(rename = "NowPlayingItem")] pub now_playing_item: Option, #[serde(rename = "DeviceId")] pub device_id: String, #[serde(rename = "ApplicationVersion")] pub application_version: String, #[serde(rename = "IsActive")] pub is_active: bool, #[serde(rename = "SupportsMediaControl")] pub supports_media_control: bool, #[serde(rename = "SupportsRemoteControl")] pub supports_remote_control: bool, #[serde(rename = "HasCustomDeviceName")] pub has_custom_device_name: bool, #[serde(rename = "ServerId")] pub server_id: String, #[serde(rename = "SupportedCommands")] pub supported_commands: Vec, } #[derive(Debug, Serialize)] pub struct PlayState { #[serde(rename = "CanSeek")] pub can_seek: bool, #[serde(rename = "IsPaused")] pub is_paused: bool, #[serde(rename = "IsMuted")] pub is_muted: bool, #[serde(rename = "RepeatMode")] pub repeat_mode: String, #[serde(rename = "ShuffleMode")] pub shuffle_mode: String, } #[derive(Debug, Serialize)] pub struct Capabilities { #[serde(rename = "PlayableMediaTypes")] pub playable_media_types: Vec, #[serde(rename = "SupportedCommands")] pub supported_commands: Vec, #[serde(rename = "SupportsMediaControl")] pub supports_media_control: bool, #[serde(rename = "SupportsContentUploading")] pub supports_content_uploading: bool, #[serde(rename = "SupportsPersistentIdentifier")] pub supports_persistent_identifier: bool, #[serde(rename = "SupportsSync")] pub supports_sync: bool, } pub fn verify_password(password: &str, stored_hash: &str) -> Result { // Handle PBKDF2-SHA512 format: $PBKDF2-SHA512$iterations=210000$salt$hash if stored_hash.starts_with("$PBKDF2-SHA512$") { let parts: Vec<&str> = stored_hash.split('$').collect(); if parts.len() != 5 { return Err(anyhow!("Invalid password hash format")); } let iterations_part = parts[2]; let salt_part = parts[3]; let hash_part = parts[4]; let iterations: u32 = iterations_part .strip_prefix("iterations=") .ok_or_else(|| anyhow!("Invalid iterations format"))? .parse()?; let salt = hex::decode(salt_part)?; let expected_hash = hex::decode(hash_part)?; let mut result = vec![0u8; expected_hash.len()]; pbkdf2::pbkdf2_hmac::(password.as_bytes(), &salt, iterations, &mut result); Ok(result == expected_hash) } else { // Fallback for other hash formats match PasswordHash::new(stored_hash) { Ok(parsed_hash) => Ok(Pbkdf2.verify_password(password.as_bytes(), &parsed_hash).is_ok()), Err(_) => Ok(false), } } } pub fn parse_authorization_header(auth_header: &str) -> Option<(String, String, String, String)> { // Parse MediaBrowser authorization header // Format: MediaBrowser Client="...", Version="...", DeviceId="...", Device="...", Token="..." if !auth_header.starts_with("MediaBrowser ") { return None; } let params_part = &auth_header[12..]; // Remove "MediaBrowser " let mut client = String::new(); let mut version = String::new(); let mut device_id = String::new(); let mut device = String::new(); for param in params_part.split(", ") { if let Some((key, value)) = param.split_once('=') { let value = value.trim_matches('"'); match key { "Client" => client = value.replace('+', " "), "Version" => version = value.to_string(), "DeviceId" => device_id = value.to_string(), "Device" => device = value.replace('+', " "), _ => {} } } } if !client.is_empty() && !version.is_empty() && !device_id.is_empty() && !device.is_empty() { Some((client, version, device_id, device)) } else { None } } impl Default for UserConfiguration { fn default() -> Self { Self { play_default_audio_track: true, subtitle_language_preference: String::new(), display_missing_episodes: false, grouped_folders: Vec::new(), subtitle_mode: "Default".to_string(), display_collections_view: false, enable_local_password: false, ordered_views: Vec::new(), latest_items_excludes: Vec::new(), my_media_excludes: Vec::new(), hide_played_in_latest: true, remember_audio_selections: true, remember_subtitle_selections: true, enable_next_episode_auto_play: true, cast_receiver_id: None, } } } impl Default for UserPolicy { fn default() -> Self { Self { is_administrator: true, is_hidden: false, enable_collection_management: true, enable_subtitle_management: true, enable_lyric_management: true, is_disabled: false, max_parental_rating: None, blocked_tags: Vec::new(), allowed_tags: Vec::new(), enable_user_preference_access: true, access_schedules: Vec::new(), block_unrated_items: Vec::new(), enable_remote_control_of_other_users: true, enable_shared_device_control: true, enable_remote_access: true, enable_live_tv_management: true, enable_live_tv_access: true, enable_media_playback: true, enable_audio_playback_transcoding: true, enable_video_playback_transcoding: true, enable_playback_remuxing: true, force_remote_source_transcoding: false, enable_content_deletion: true, enable_content_deletion_from_folders: Vec::new(), enable_content_downloading: true, enable_sync_transcoding: true, enable_media_conversion: true, enabled_devices: Vec::new(), enable_all_devices: true, enabled_channels: Vec::new(), enable_all_channels: true, enabled_folders: Vec::new(), enable_all_folders: true, invalid_login_attempt_count: 0, login_attempts_before_lockout: -1, max_active_sessions: 0, enable_public_sharing: true, blocked_media_folders: Vec::new(), blocked_channels: Vec::new(), remote_client_bitrate_limit: 0, authentication_provider_id: "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider".to_string(), password_reset_provider_id: "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider".to_string(), sync_play_access: "CreateAndJoinGroups".to_string(), } } }