fix websocket session auth and header parsing for jellyfin dashboard

This commit is contained in:
2026-02-02 20:41:25 -05:00
parent b40349206d
commit 83063f594a
3 changed files with 100 additions and 27 deletions

View File

@@ -3308,9 +3308,20 @@ public class JellyfinController : ControllerBase
string? device = null; string? device = null;
string? version = null; string? version = null;
if (headers.TryGetValue("Authorization", out var authHeader)) // Check X-Emby-Authorization FIRST (most Jellyfin clients use this)
// Then fall back to Authorization header
string? authStr = null;
if (headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
{
authStr = embyAuthHeader.ToString();
}
else if (headers.TryGetValue("Authorization", out var authHeader))
{
authStr = authHeader.ToString();
}
if (!string.IsNullOrEmpty(authStr))
{ {
var authStr = authHeader.ToString();
// Parse: MediaBrowser Client="...", Device="...", DeviceId="...", Version="..." // Parse: MediaBrowser Client="...", Device="...", DeviceId="...", Version="..."
var parts = authStr.Replace("MediaBrowser ", "").Split(','); var parts = authStr.Replace("MediaBrowser ", "").Split(',');
foreach (var part in parts) foreach (var part in parts)

View File

@@ -89,17 +89,28 @@ public class WebSocketProxyMiddleware
// Connect to Jellyfin WebSocket // Connect to Jellyfin WebSocket
serverWebSocket = new ClientWebSocket(); serverWebSocket = new ClientWebSocket();
// Forward authentication headers // Forward authentication headers - check X-Emby-Authorization FIRST
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader)) // Most Jellyfin clients use X-Emby-Authorization, not Authorization
{ if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
serverWebSocket.Options.SetRequestHeader("Authorization", authHeader.ToString());
_logger.LogWarning("🔑 WEBSOCKET: Forwarded Authorization header");
}
else if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
{ {
serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuthHeader.ToString()); serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuthHeader.ToString());
_logger.LogWarning("🔑 WEBSOCKET: Forwarded X-Emby-Authorization header"); _logger.LogWarning("🔑 WEBSOCKET: Forwarded X-Emby-Authorization header");
} }
else if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
{
var authValue = authHeader.ToString();
// If it's a MediaBrowser auth header, use X-Emby-Authorization
if (authValue.Contains("MediaBrowser", StringComparison.OrdinalIgnoreCase))
{
serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", authValue);
_logger.LogWarning("🔑 WEBSOCKET: Converted Authorization to X-Emby-Authorization header");
}
else
{
serverWebSocket.Options.SetRequestHeader("Authorization", authValue);
_logger.LogWarning("🔑 WEBSOCKET: Forwarded Authorization header");
}
}
// Set user agent // Set user agent
serverWebSocket.Options.SetRequestHeader("User-Agent", "Allstarr/1.0"); serverWebSocket.Options.SetRequestHeader("User-Agent", "Allstarr/1.0");

View File

@@ -200,18 +200,16 @@ public class JellyfinSessionManager : IDisposable
var jellyfinHost = jellyfinUrl.Replace("https://", "").Replace("http://", ""); var jellyfinHost = jellyfinUrl.Replace("https://", "").Replace("http://", "");
var jellyfinWsUrl = $"{wsScheme}{jellyfinHost}/socket"; var jellyfinWsUrl = $"{wsScheme}{jellyfinHost}/socket";
// Add API key if available // IMPORTANT: Do NOT add api_key to URL - we want to authenticate as the CLIENT, not the server
if (!string.IsNullOrEmpty(_settings.ApiKey)) // The client's token is passed via X-Emby-Authorization header
{ // Using api_key would create a session for the server/admin, not the actual user's client
jellyfinWsUrl += $"?api_key={_settings.ApiKey}";
}
_logger.LogWarning("🔗 WEBSOCKET: Connecting to Jellyfin for device {DeviceId}: {Url}", deviceId, jellyfinWsUrl); _logger.LogWarning("🔗 WEBSOCKET: Connecting to Jellyfin for device {DeviceId}: {Url}", deviceId, jellyfinWsUrl);
webSocket = new ClientWebSocket(); webSocket = new ClientWebSocket();
session.WebSocket = webSocket; session.WebSocket = webSocket;
// Forward authentication headers // Forward authentication headers from the CLIENT - this is critical for session to appear under the right user
if (headers.TryGetValue("X-Emby-Authorization", out var embyAuth)) if (headers.TryGetValue("X-Emby-Authorization", out var embyAuth))
{ {
webSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuth.ToString()); webSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuth.ToString());
@@ -231,6 +229,15 @@ public class JellyfinSessionManager : IDisposable
_logger.LogWarning("🔑 WEBSOCKET: Forwarded Authorization for {DeviceId}", deviceId); _logger.LogWarning("🔑 WEBSOCKET: Forwarded Authorization for {DeviceId}", deviceId);
} }
} }
else
{
// No client auth found - fall back to server API key as last resort
if (!string.IsNullOrEmpty(_settings.ApiKey))
{
jellyfinWsUrl += $"?api_key={_settings.ApiKey}";
_logger.LogWarning("⚠️ WEBSOCKET: No client auth found, falling back to server API key for {DeviceId}", deviceId);
}
}
// Set user agent // Set user agent
webSocket.Options.SetRequestHeader("User-Agent", $"Allstarr-Proxy/{session.Client}"); webSocket.Options.SetRequestHeader("User-Agent", $"Allstarr-Proxy/{session.Client}");
@@ -239,13 +246,36 @@ public class JellyfinSessionManager : IDisposable
await webSocket.ConnectAsync(new Uri(jellyfinWsUrl), CancellationToken.None); await webSocket.ConnectAsync(new Uri(jellyfinWsUrl), CancellationToken.None);
_logger.LogWarning("✓ WEBSOCKET: Connected to Jellyfin for device {DeviceId}", deviceId); _logger.LogWarning("✓ WEBSOCKET: Connected to Jellyfin for device {DeviceId}", deviceId);
// Keep the WebSocket alive by reading messages // CRITICAL: Send ForceKeepAlive message to initialize session in Jellyfin
// This tells Jellyfin to create/show the session in the dashboard
// Without this message, the WebSocket is connected but no session appears
var forceKeepAliveMessage = "{\"MessageType\":\"ForceKeepAlive\",\"Data\":100}";
var messageBytes = Encoding.UTF8.GetBytes(forceKeepAliveMessage);
await webSocket.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, CancellationToken.None);
_logger.LogWarning("📤 WEBSOCKET: Sent ForceKeepAlive to initialize session for {DeviceId}", deviceId);
// Also send SessionsStart to subscribe to session updates
var sessionsStartMessage = "{\"MessageType\":\"SessionsStart\",\"Data\":\"0,1500\"}";
messageBytes = Encoding.UTF8.GetBytes(sessionsStartMessage);
await webSocket.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, CancellationToken.None);
_logger.LogWarning("📤 WEBSOCKET: Sent SessionsStart for {DeviceId}", deviceId);
// Keep the WebSocket alive by reading messages and sending periodic keep-alive
var buffer = new byte[1024 * 4]; var buffer = new byte[1024 * 4];
var lastKeepAlive = DateTime.UtcNow;
using var cts = new CancellationTokenSource();
while (webSocket.State == WebSocketState.Open && _sessions.ContainsKey(deviceId)) while (webSocket.State == WebSocketState.Open && _sessions.ContainsKey(deviceId))
{ {
try try
{ {
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); // Use a timeout so we can send keep-alive messages periodically
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), timeoutCts.Token);
if (result.MessageType == WebSocketMessageType.Close) if (result.MessageType == WebSocketMessageType.Close)
{ {
@@ -259,6 +289,27 @@ public class JellyfinSessionManager : IDisposable
var message = Encoding.UTF8.GetString(buffer, 0, result.Count); var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
_logger.LogWarning("📥 WEBSOCKET: Received from Jellyfin for {DeviceId}: {Message}", _logger.LogWarning("📥 WEBSOCKET: Received from Jellyfin for {DeviceId}: {Message}",
deviceId, message.Length > 100 ? message[..100] + "..." : message); deviceId, message.Length > 100 ? message[..100] + "..." : message);
// Respond to KeepAlive requests from Jellyfin
if (message.Contains("\"MessageType\":\"KeepAlive\""))
{
_logger.LogWarning("💓 WEBSOCKET: Received KeepAlive from Jellyfin for {DeviceId}", deviceId);
}
}
}
catch (OperationCanceledException) when (!cts.IsCancellationRequested)
{
// Timeout - this is expected, send keep-alive if needed
}
// Send periodic keep-alive every 30 seconds
if (DateTime.UtcNow - lastKeepAlive > TimeSpan.FromSeconds(30))
{
var keepAliveMsg = "{\"MessageType\":\"KeepAlive\"}";
var keepAliveBytes = Encoding.UTF8.GetBytes(keepAliveMsg);
await webSocket.SendAsync(new ArraySegment<byte>(keepAliveBytes), WebSocketMessageType.Text, true, CancellationToken.None);
_logger.LogWarning("💓 WEBSOCKET: Sent KeepAlive for {DeviceId}", deviceId);
lastKeepAlive = DateTime.UtcNow;
} }
} }
catch (WebSocketException wsEx) catch (WebSocketException wsEx)