diff options
7 files changed, 219 insertions, 138 deletions
diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 23e94afd7..949c3b505 100644 --- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Cryptography; @@ -10,7 +9,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Security; using MediaBrowser.Model.QuickConnect; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using MediaBrowser.Common; using Microsoft.Extensions.Logging; using MediaBrowser.Common.Extensions; @@ -163,7 +162,7 @@ namespace Emby.Server.Implementations.QuickConnect } /// <inheritdoc/> - public bool AuthorizeRequest(IRequest request, string code) + public bool AuthorizeRequest(HttpRequest request, string code) { ExpireRequests(); AssertActive(); diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs new file mode 100644 index 000000000..d45ea058d --- /dev/null +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -0,0 +1,160 @@ +using System.ComponentModel.DataAnnotations; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Model.QuickConnect; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Quick connect controller. + /// </summary> + public class QuickConnectController : BaseJellyfinApiController + { + private readonly IQuickConnect _quickConnect; + private readonly IUserManager _userManager; + private readonly IAuthorizationContext _authContext; + + /// <summary> + /// Initializes a new instance of the <see cref="QuickConnectController"/> class. + /// </summary> + /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param> + /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> + /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> + public QuickConnectController( + IQuickConnect quickConnect, + IUserManager userManager, + IAuthorizationContext authContext) + { + _quickConnect = quickConnect; + _userManager = userManager; + _authContext = authContext; + } + + /// <summary> + /// Gets the current quick connect state. + /// </summary> + /// <response code="200">Quick connect state returned.</response> + /// <returns>The current <see cref="QuickConnectState"/>.</returns> + [HttpGet("Status")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<QuickConnectState> GetStatus() + { + _quickConnect.ExpireRequests(); + return Ok(_quickConnect.State); + } + + /// <summary> + /// Initiate a new quick connect request. + /// </summary> + /// <param name="friendlyName">Device friendly name.</param> + /// <response code="200">Quick connect request successfully created.</response> + /// <response code="401">Quick connect is not active on this server.</response> + /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> + [HttpGet("Initiate")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<QuickConnectResult> Initiate([FromQuery] string? friendlyName) + { + return Ok(_quickConnect.TryConnect(friendlyName)); + } + + /// <summary> + /// Attempts to retrieve authentication information. + /// </summary> + /// <param name="secret">Secret previously returned from the Initiate endpoint.</param> + /// <response code="200">Quick connect result returned.</response> + /// <response code="404">Unknown quick connect secret.</response> + /// <returns>An updated <see cref="QuickConnectResult"/>.</returns> + [HttpGet("Connect")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<QuickConnectResult> Connect([FromQuery] string? secret) + { + try + { + var result = _quickConnect.CheckRequestStatus(secret); + return Ok(result); + } + catch (ResourceNotFoundException) + { + return NotFound("Unknown secret"); + } + } + + /// <summary> + /// Temporarily activates quick connect for five minutes. + /// </summary> + /// <response code="204">Quick connect has been temporarily activated.</response> + /// <response code="403">Quick connect is unavailable on this server.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpPost("Activate")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult Activate() + { + if (_quickConnect.State == QuickConnectState.Unavailable) + { + return Forbid("Quick connect is unavailable"); + } + + _quickConnect.Activate(); + return NoContent(); + } + + /// <summary> + /// Enables or disables quick connect. + /// </summary> + /// <param name="status">New <see cref="QuickConnectState"/>.</param> + /// <response code="204">Quick connect state set successfully.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpPost("Available")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Available([FromQuery] QuickConnectState? status) + { + _quickConnect.SetState(status ?? QuickConnectState.Available); + return NoContent(); + } + + /// <summary> + /// Authorizes a pending quick connect request. + /// </summary> + /// <param name="code">Quick connect code to authorize.</param> + /// <response code="200">Quick connect result authorized successfully.</response> + /// <response code="400">Missing quick connect code.</response> + /// <returns>Boolean indicating if the authorization was successful.</returns> + [HttpPost("Authorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public ActionResult<bool> Authorize([FromQuery, Required] string? code) + { + if (code == null) + { + return BadRequest("Missing code"); + } + + return Ok(_quickConnect.AuthorizeRequest(Request, code)); + } + + /// <summary> + /// Deauthorize all quick connect devices for the current user. + /// </summary> + /// <response code="200">All quick connect devices were deleted.</response> + /// <returns>The number of devices that were deleted.</returns> + [HttpPost("Deauthorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<int> Deauthorize() + { + var userId = _authContext.GetAuthorizationInfo(Request).UserId; + return _quickConnect.DeleteAllDevices(userId); + } + } +} diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 272312522..131fffb7a 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -217,6 +217,47 @@ namespace Jellyfin.Api.Controllers } /// <summary> + /// Authenticates a user with quick connect. + /// </summary> + /// <param name="request">The <see cref="QuickConnectDto"/> request.</param> + /// <response code="200">User authenticated.</response> + /// <response code="400">Missing token.</response> + /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns> + [HttpPost("AuthenticateWithQuickConnect")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) + { + if (request.Token == null) + { + return BadRequest("Access token is required."); + } + + var auth = _authContext.GetAuthorizationInfo(Request); + + try + { + var authRequest = new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device, + }; + + var result = await _sessionManager.AuthenticateQuickConnect( + authRequest, + request.Token).ConfigureAwait(false); + + return result; + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + } + } + + /// <summary> /// Updates a user's password. /// </summary> /// <param name="userId">The user id.</param> diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs new file mode 100644 index 000000000..8f53d5f37 --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs @@ -0,0 +1,13 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// <summary> + /// The quick connect request body. + /// </summary> + public class QuickConnectDto + { + /// <summary> + /// Gets or sets the quick connect token. + /// </summary> + public string? Token { get; set; } + } +} diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs deleted file mode 100644 index 7093be990..000000000 --- a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.QuickConnect; -using MediaBrowser.Model.QuickConnect; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.QuickConnect -{ - [Route("/QuickConnect/Initiate", "GET", Summary = "Requests a new quick connect code")] - public class Initiate : IReturn<QuickConnectResult> - { - [ApiMember(Name = "FriendlyName", Description = "Device friendly name", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string FriendlyName { get; set; } - } - - [Route("/QuickConnect/Connect", "GET", Summary = "Attempts to retrieve authentication information")] - public class Connect : IReturn<QuickConnectResult> - { - [ApiMember(Name = "Secret", Description = "Quick connect secret", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Secret { get; set; } - } - - [Route("/QuickConnect/Authorize", "POST", Summary = "Authorizes a pending quick connect request")] - [Authenticated] - public class Authorize : IReturn<bool> - { - [ApiMember(Name = "Code", Description = "Quick connect identifying code", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Code { get; set; } - } - - [Route("/QuickConnect/Deauthorize", "POST", Summary = "Deletes all quick connect authorization tokens for the current user")] - [Authenticated] - public class Deauthorize : IReturn<int> - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public Guid UserId { get; set; } - } - - [Route("/QuickConnect/Status", "GET", Summary = "Gets the current quick connect state")] - public class QuickConnectStatus : IReturn<QuickConnectResult> - { - - } - - [Route("/QuickConnect/Available", "POST", Summary = "Enables or disables quick connect")] - [Authenticated(Roles = "Admin")] - public class Available : IReturn<QuickConnectState> - { - [ApiMember(Name = "Status", Description = "New quick connect status", IsRequired = false, DataType = "QuickConnectState", ParameterType = "query", Verb = "GET")] - public QuickConnectState Status { get; set; } - } - - [Route("/QuickConnect/Activate", "POST", Summary = "Temporarily activates quick connect for the time period defined in the server configuration")] - [Authenticated] - public class Activate : IReturn<bool> - { - - } - - public class QuickConnectService : BaseApiService - { - private IQuickConnect _quickConnect; - private IUserManager _userManager; - private IAuthorizationContext _authContext; - - public QuickConnectService( - ILogger<QuickConnectService> logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IUserManager userManager, - IAuthorizationContext authContext, - IQuickConnect quickConnect) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _userManager = userManager; - _quickConnect = quickConnect; - _authContext = authContext; - } - - public object Get(Initiate request) - { - return _quickConnect.TryConnect(request.FriendlyName); - } - - public object Get(Connect request) - { - return _quickConnect.CheckRequestStatus(request.Secret); - } - - public object Get(QuickConnectStatus request) - { - _quickConnect.ExpireRequests(); - return _quickConnect.State; - } - - public object Post(Deauthorize request) - { - AssertCanUpdateUser(_authContext, _userManager, request.UserId, true); - - return _quickConnect.DeleteAllDevices(request.UserId); - } - - public object Post(Authorize request) - { - return _quickConnect.AuthorizeRequest(Request, request.Code); - } - - public object Post(Activate request) - { - if (_quickConnect.State == QuickConnectState.Unavailable) - { - return false; - } - - string name = _authContext.GetAuthorizationInfo(Request).User.Username; - - Logger.LogInformation("{name} temporarily activated quick connect", name); - _quickConnect.Activate(); - - return true; - } - - public object Post(Available request) - { - _quickConnect.SetState(request.Status); - return _quickConnect.State; - } - } -} diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs index 993637c8a..fd7e973f6 100644 --- a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using MediaBrowser.Model.QuickConnect; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.QuickConnect { @@ -66,7 +65,7 @@ namespace MediaBrowser.Controller.QuickConnect /// <param name="request">HTTP request object.</param> /// <param name="code">Identifying code for the request.</param> /// <returns>A boolean indicating if the authorization completed successfully.</returns> - bool AuthorizeRequest(IRequest request, string code); + bool AuthorizeRequest(HttpRequest request, string code); /// <summary> /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired. diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index ffa19fb69..d44787b88 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -268,6 +268,7 @@ namespace MediaBrowser.Controller.Session /// Authenticates a new session with quick connect. /// </summary> /// <param name="request">The request.</param> + /// <param name="token">Quick connect access token.</param> /// <returns>Task{SessionInfo}.</returns> Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token); |
