391 lines
14 KiB
Rust
391 lines
14 KiB
Rust
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<String>,
|
|
#[serde(rename = "LastActivityDate")]
|
|
pub last_activity_date: Option<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[serde(rename = "LatestItemsExcludes")]
|
|
pub latest_items_excludes: Vec<String>,
|
|
#[serde(rename = "MyMediaExcludes")]
|
|
pub my_media_excludes: Vec<String>,
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<i32>,
|
|
#[serde(rename = "BlockedTags")]
|
|
pub blocked_tags: Vec<String>,
|
|
#[serde(rename = "AllowedTags")]
|
|
pub allowed_tags: Vec<String>,
|
|
#[serde(rename = "EnableUserPreferenceAccess")]
|
|
pub enable_user_preference_access: bool,
|
|
#[serde(rename = "AccessSchedules")]
|
|
pub access_schedules: Vec<String>,
|
|
#[serde(rename = "BlockUnratedItems")]
|
|
pub block_unrated_items: Vec<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[serde(rename = "EnableAllDevices")]
|
|
pub enable_all_devices: bool,
|
|
#[serde(rename = "EnabledChannels")]
|
|
pub enabled_channels: Vec<String>,
|
|
#[serde(rename = "EnableAllChannels")]
|
|
pub enable_all_channels: bool,
|
|
#[serde(rename = "EnabledFolders")]
|
|
pub enabled_folders: Vec<String>,
|
|
#[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<String>,
|
|
#[serde(rename = "BlockedChannels")]
|
|
pub blocked_channels: Vec<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[serde(rename = "SupportedCommands")]
|
|
pub supported_commands: Vec<String>,
|
|
#[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<bool> {
|
|
// 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::<sha2::Sha512>(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(),
|
|
}
|
|
}
|
|
}
|