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? 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)
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,26 +246,70 @@ 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);
|
||||||
if (result.MessageType == WebSocketMessageType.Close)
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_logger.LogWarning("🔌 WEBSOCKET: Jellyfin closed WebSocket for device {DeviceId}", deviceId);
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), timeoutCts.Token);
|
||||||
break;
|
|
||||||
|
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)
|
||||||
// Log received messages for debugging
|
|
||||||
if (result.MessageType == WebSocketMessageType.Text)
|
|
||||||
{
|
{
|
||||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
// Timeout - this is expected, send keep-alive if needed
|
||||||
_logger.LogWarning("📥 WEBSOCKET: Received from Jellyfin for {DeviceId}: {Message}",
|
}
|
||||||
deviceId, message.Length > 100 ? message[..100] + "..." : message);
|
|
||||||
|
// 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user