mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
fix websocket session auth and header parsing for jellyfin dashboard
This commit is contained in:
@@ -3308,9 +3308,20 @@ public class JellyfinController : ControllerBase
|
||||
string? device = 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="..."
|
||||
var parts = authStr.Replace("MediaBrowser ", "").Split(',');
|
||||
foreach (var part in parts)
|
||||
|
||||
@@ -89,17 +89,28 @@ public class WebSocketProxyMiddleware
|
||||
// Connect to Jellyfin WebSocket
|
||||
serverWebSocket = new ClientWebSocket();
|
||||
|
||||
// Forward authentication headers
|
||||
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
serverWebSocket.Options.SetRequestHeader("Authorization", authHeader.ToString());
|
||||
_logger.LogWarning("🔑 WEBSOCKET: Forwarded Authorization header");
|
||||
}
|
||||
else if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
|
||||
// Forward authentication headers - check X-Emby-Authorization FIRST
|
||||
// Most Jellyfin clients use X-Emby-Authorization, not Authorization
|
||||
if (context.Request.Headers.TryGetValue("X-Emby-Authorization", out var embyAuthHeader))
|
||||
{
|
||||
serverWebSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuthHeader.ToString());
|
||||
_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
|
||||
serverWebSocket.Options.SetRequestHeader("User-Agent", "Allstarr/1.0");
|
||||
|
||||
@@ -200,18 +200,16 @@ public class JellyfinSessionManager : IDisposable
|
||||
var jellyfinHost = jellyfinUrl.Replace("https://", "").Replace("http://", "");
|
||||
var jellyfinWsUrl = $"{wsScheme}{jellyfinHost}/socket";
|
||||
|
||||
// Add API key if available
|
||||
if (!string.IsNullOrEmpty(_settings.ApiKey))
|
||||
{
|
||||
jellyfinWsUrl += $"?api_key={_settings.ApiKey}";
|
||||
}
|
||||
// IMPORTANT: Do NOT add api_key to URL - we want to authenticate as the CLIENT, not the server
|
||||
// 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
|
||||
|
||||
_logger.LogWarning("🔗 WEBSOCKET: Connecting to Jellyfin for device {DeviceId}: {Url}", deviceId, jellyfinWsUrl);
|
||||
|
||||
webSocket = new ClientWebSocket();
|
||||
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))
|
||||
{
|
||||
webSocket.Options.SetRequestHeader("X-Emby-Authorization", embyAuth.ToString());
|
||||
@@ -231,6 +229,15 @@ public class JellyfinSessionManager : IDisposable
|
||||
_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
|
||||
webSocket.Options.SetRequestHeader("User-Agent", $"Allstarr-Proxy/{session.Client}");
|
||||
@@ -239,26 +246,70 @@ public class JellyfinSessionManager : IDisposable
|
||||
await webSocket.ConnectAsync(new Uri(jellyfinWsUrl), CancellationToken.None);
|
||||
_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 lastKeepAlive = DateTime.UtcNow;
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
while (webSocket.State == WebSocketState.Open && _sessions.ContainsKey(deviceId))
|
||||
{
|
||||
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));
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("🔌 WEBSOCKET: Jellyfin closed WebSocket for device {DeviceId}", deviceId);
|
||||
break;
|
||||
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), timeoutCts.Token);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
_logger.LogWarning("🔌 WEBSOCKET: Jellyfin closed WebSocket for device {DeviceId}", deviceId);
|
||||
break;
|
||||
}
|
||||
|
||||
// Log received messages for debugging
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
_logger.LogWarning("📥 WEBSOCKET: Received from Jellyfin for {DeviceId}: {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
|
||||
}
|
||||
|
||||
// Log received messages for debugging
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
// Send periodic keep-alive every 30 seconds
|
||||
if (DateTime.UtcNow - lastKeepAlive > TimeSpan.FromSeconds(30))
|
||||
{
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
_logger.LogWarning("📥 WEBSOCKET: Received from Jellyfin for {DeviceId}: {Message}",
|
||||
deviceId, message.Length > 100 ? message[..100] + "..." : message);
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user