aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/HttpServer
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/HttpServer')
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs9
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs291
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs21
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs68
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketManager.cs11
5 files changed, 58 insertions, 342 deletions
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 4a0fc8239..1d04f3da3 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -1,8 +1,7 @@
#pragma warning disable CS1591
-using System;
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@@ -18,13 +17,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
- public AuthorizationInfo Authenticate(HttpRequest request)
+ public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{
- var auth = _authorizationContext.GetAuthorizationInfo(request);
+ var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken)
{
- throw new AuthenticationException("Request does not contain a token.");
+ return auth;
}
if (!auth.IsAuthenticated)
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
deleted file mode 100644
index 024404ceb..000000000
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ /dev/null
@@ -1,291 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.HttpServer.Security
-{
- public class AuthorizationContext : IAuthorizationContext
- {
- private readonly IAuthenticationRepository _authRepo;
- private readonly IUserManager _userManager;
-
- public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
- {
- _authRepo = authRepo;
- _userManager = userManager;
- }
-
- public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
- {
- if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
- {
- return (AuthorizationInfo)cached;
- }
-
- return GetAuthorization(requestContext);
- }
-
- public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
- {
- var auth = GetAuthorizationDictionary(requestContext);
- var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
- return authInfo;
- }
-
- /// <summary>
- /// Gets the authorization.
- /// </summary>
- /// <param name="httpReq">The HTTP req.</param>
- /// <returns>Dictionary{System.StringSystem.String}.</returns>
- private AuthorizationInfo GetAuthorization(HttpContext httpReq)
- {
- var auth = GetAuthorizationDictionary(httpReq);
- var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
-
- httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
- return authInfo;
- }
-
- private AuthorizationInfo GetAuthorizationInfoFromDictionary(
- in Dictionary<string, string> auth,
- in IHeaderDictionary headers,
- in IQueryCollection queryString)
- {
- string deviceId = null;
- string device = null;
- string client = null;
- string version = null;
- string token = null;
-
- if (auth != null)
- {
- auth.TryGetValue("DeviceId", out deviceId);
- auth.TryGetValue("Device", out device);
- auth.TryGetValue("Client", out client);
- auth.TryGetValue("Version", out version);
- auth.TryGetValue("Token", out token);
- }
-
- if (string.IsNullOrEmpty(token))
- {
- token = headers["X-Emby-Token"];
- }
-
- if (string.IsNullOrEmpty(token))
- {
- token = headers["X-MediaBrowser-Token"];
- }
-
- if (string.IsNullOrEmpty(token))
- {
- token = queryString["ApiKey"];
- }
-
- // TODO deprecate this query parameter.
- if (string.IsNullOrEmpty(token))
- {
- token = queryString["api_key"];
- }
-
- var authInfo = new AuthorizationInfo
- {
- Client = client,
- Device = device,
- DeviceId = deviceId,
- Version = version,
- Token = token,
- IsAuthenticated = false,
- HasToken = false
- };
-
- if (string.IsNullOrWhiteSpace(token))
- {
- // Request doesn't contain a token.
- return authInfo;
- }
-
- authInfo.HasToken = true;
- var result = _authRepo.Get(new AuthenticationInfoQuery
- {
- AccessToken = token
- });
-
- if (result.Items.Count > 0)
- {
- authInfo.IsAuthenticated = true;
- }
-
- var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
-
- if (originalAuthenticationInfo != null)
- {
- var updateToken = false;
-
- // TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(authInfo.Client))
- {
- authInfo.Client = originalAuthenticationInfo.AppName;
- }
-
- if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
- {
- authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
- }
-
- // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
-
- if (string.IsNullOrWhiteSpace(authInfo.Device))
- {
- authInfo.Device = originalAuthenticationInfo.DeviceName;
- }
- else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
- {
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- originalAuthenticationInfo.DeviceName = authInfo.Device;
- }
- }
-
- if (string.IsNullOrWhiteSpace(authInfo.Version))
- {
- authInfo.Version = originalAuthenticationInfo.AppVersion;
- }
- else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
- {
- if (allowTokenInfoUpdate)
- {
- updateToken = true;
- originalAuthenticationInfo.AppVersion = authInfo.Version;
- }
- }
-
- if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
- {
- originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
- updateToken = true;
- }
-
- if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
- {
- authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
-
- if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
- {
- originalAuthenticationInfo.UserName = authInfo.User.Username;
- updateToken = true;
- }
-
- authInfo.IsApiKey = false;
- }
- else
- {
- authInfo.IsApiKey = true;
- }
-
- if (updateToken)
- {
- _authRepo.Update(originalAuthenticationInfo);
- }
- }
-
- return authInfo;
- }
-
- /// <summary>
- /// Gets the auth.
- /// </summary>
- /// <param name="httpReq">The HTTP req.</param>
- /// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
- {
- var auth = httpReq.Request.Headers["X-Emby-Authorization"];
-
- if (string.IsNullOrEmpty(auth))
- {
- auth = httpReq.Request.Headers[HeaderNames.Authorization];
- }
-
- return GetAuthorization(auth);
- }
-
- /// <summary>
- /// Gets the auth.
- /// </summary>
- /// <param name="httpReq">The HTTP req.</param>
- /// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
- {
- var auth = httpReq.Headers["X-Emby-Authorization"];
-
- if (string.IsNullOrEmpty(auth))
- {
- auth = httpReq.Headers[HeaderNames.Authorization];
- }
-
- return GetAuthorization(auth);
- }
-
- /// <summary>
- /// Gets the authorization.
- /// </summary>
- /// <param name="authorizationHeader">The authorization header.</param>
- /// <returns>Dictionary{System.StringSystem.String}.</returns>
- private Dictionary<string, string> GetAuthorization(string authorizationHeader)
- {
- if (authorizationHeader == null)
- {
- return null;
- }
-
- var parts = authorizationHeader.Split(' ', 2);
-
- // There should be at least to parts
- if (parts.Length != 2)
- {
- return null;
- }
-
- var acceptedNames = new[] { "MediaBrowser", "Emby" };
-
- // It has to be a digest request
- if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
- {
- return null;
- }
-
- // Remove uptil the first space
- authorizationHeader = parts[1];
- parts = authorizationHeader.Split(',');
-
- var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- foreach (var item in parts)
- {
- var param = item.Trim().Split('=', 2);
-
- if (param.Length == 2)
- {
- var value = NormalizeValue(param[1].Trim('"'));
- result[param[0]] = value;
- }
- }
-
- return result;
- }
-
- private static string NormalizeValue(string value)
- {
- return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
- }
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 040b6b9e4..a7647caf9 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
@@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
- public SessionInfo GetSession(HttpContext requestContext)
+ public async Task<SessionInfo> GetSession(HttpContext requestContext)
{
- var authorization = _authContext.GetAuthorizationInfo(requestContext);
+ var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var user = authorization.User;
- return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
+ return await _sessionManager.LogSessionActivity(
+ authorization.Client,
+ authorization.Version,
+ authorization.DeviceId,
+ authorization.Device,
+ requestContext.GetNormalizedRemoteIp().ToString(),
+ user).ConfigureAwait(false);
}
- public SessionInfo GetSession(object requestContext)
+ public Task<SessionInfo> GetSession(object requestContext)
{
return GetSession((HttpContext)requestContext);
}
- public User GetUser(HttpContext requestContext)
+ public async Task<User?> GetUser(HttpContext requestContext)
{
- var session = GetSession(requestContext);
+ var session = await GetSession(requestContext).ConfigureAwait(false);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
- public User GetUser(object requestContext)
+ public Task<User?> GetUser(object requestContext)
{
return GetUser(((HttpRequest)requestContext).HttpContext);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index fed2addf8..5f25f6980 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,14 +1,13 @@
-#nullable enable
-
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
@@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
QueryString = query;
- _jsonOptions = JsonDefaults.GetOptions();
+ _jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now;
}
@@ -63,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
public event EventHandler<EventArgs>? Closed;
/// <summary>
- /// Gets or sets the remote end point.
+ /// Gets the remote end point.
/// </summary>
public IPAddress? RemoteEndPoint { get; }
@@ -83,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer
public DateTime LastKeepAliveDate { get; set; }
/// <summary>
- /// Gets or sets the query string.
+ /// Gets the query string.
/// </summary>
/// <value>The query string.</value>
public IQueryCollection QueryString { get; }
@@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Sends a message asynchronously.
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type of the message.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
@@ -138,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
writer.Advance(bytesRead);
// Make the data available to the PipeReader
- FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
+ FlushResult flushResult = await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
if (flushResult.IsCompleted)
{
// The PipeReader stopped reading
@@ -151,8 +150,8 @@ namespace Emby.Server.Implementations.HttpServer
{
await ProcessInternal(pipe.Reader).ConfigureAwait(false);
}
- } while (
- (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+ }
+ while ((_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
&& receiveresult.MessageType != WebSocketMessageType.Close);
Closed?.Invoke(this, EventArgs.Empty);
@@ -181,32 +180,16 @@ namespace Emby.Server.Implementations.HttpServer
}
WebSocketMessage<object>? stub;
+ long bytesConsumed = 0;
try
{
-
- if (buffer.IsSingleSegment)
- {
- stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
- }
- else
- {
- var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
- try
- {
- buffer.CopyTo(buf);
- stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
- }
- finally
- {
- ArrayPool<byte>.Shared.Return(buf);
- }
- }
+ stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
}
catch (JsonException ex)
{
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
- _logger.LogError(ex, "Error processing web socket message");
+ _logger.LogError(ex, "Error processing web socket message: {Data}", Encoding.UTF8.GetString(buffer));
return;
}
@@ -217,27 +200,34 @@ namespace Emby.Server.Implementations.HttpServer
}
// Tell the PipeReader how much of the buffer we have consumed
- reader.AdvanceTo(buffer.End);
+ reader.AdvanceTo(buffer.GetPosition(bytesConsumed));
_logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
- var info = new WebSocketMessageInfo
- {
- MessageType = stub.MessageType,
- Data = stub.Data?.ToString(), // Data can be null
- Connection = this
- };
-
- if (info.MessageType == SessionMessageType.KeepAlive)
+ if (stub.MessageType == SessionMessageType.KeepAlive)
{
await SendKeepAliveResponse().ConfigureAwait(false);
}
else
{
- await OnReceive(info).ConfigureAwait(false);
+ await OnReceive(
+ new WebSocketMessageInfo
+ {
+ MessageType = stub.MessageType,
+ Data = stub.Data?.ToString(), // Data can be null
+ Connection = this
+ }).ConfigureAwait(false);
}
}
+ internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
+ {
+ var jsonReader = new Utf8JsonReader(bytes);
+ var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
+ bytesConsumed = jsonReader.BytesConsumed;
+ return ret;
+ }
+
private Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index d6cf6233e..e99876dce 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -14,15 +16,18 @@ namespace Emby.Server.Implementations.HttpServer
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
+ private readonly IAuthService _authService;
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
public WebSocketManager(
+ IAuthService authService,
IEnumerable<IWebSocketListener> webSocketListeners,
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
_webSocketListeners = webSocketListeners.ToArray();
+ _authService = authService;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -30,6 +35,12 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
+ var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
+ if (!authorizationInfo.IsAuthenticated)
+ {
+ throw new SecurityException("Token is required");
+ }
+
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);