From 035d29fb357006c29ffb40e0a53c1e999237cdd1 Mon Sep 17 00:00:00 2001 From: Matt Montgomery <33811686+ConfusedPolarBear@users.noreply.github.com> Date: Thu, 13 Aug 2020 15:35:04 -0500 Subject: [PATCH] Migrate to new API standard --- .../QuickConnect/QuickConnectManager.cs | 5 +- .../Controllers/QuickConnectController.cs | 160 ++++++++++++++++++ Jellyfin.Api/Controllers/UserController.cs | 41 +++++ .../Models/UserDtos/QuickConnectDto.cs | 13 ++ .../QuickConnect/QuickConnectService.cs | 132 --------------- .../QuickConnect/IQuickConnect.cs | 5 +- .../Session/ISessionManager.cs | 1 + 7 files changed, 219 insertions(+), 138 deletions(-) create mode 100644 Jellyfin.Api/Controllers/QuickConnectController.cs create mode 100644 Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs delete mode 100644 MediaBrowser.Api/QuickConnect/QuickConnectService.cs diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs index 23e94afd72..949c3b5058 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 } /// - 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 0000000000..d45ea058d2 --- /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 +{ + /// + /// Quick connect controller. + /// + public class QuickConnectController : BaseJellyfinApiController + { + private readonly IQuickConnect _quickConnect; + private readonly IUserManager _userManager; + private readonly IAuthorizationContext _authContext; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public QuickConnectController( + IQuickConnect quickConnect, + IUserManager userManager, + IAuthorizationContext authContext) + { + _quickConnect = quickConnect; + _userManager = userManager; + _authContext = authContext; + } + + /// + /// Gets the current quick connect state. + /// + /// Quick connect state returned. + /// The current . + [HttpGet("Status")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStatus() + { + _quickConnect.ExpireRequests(); + return Ok(_quickConnect.State); + } + + /// + /// Initiate a new quick connect request. + /// + /// Device friendly name. + /// Quick connect request successfully created. + /// Quick connect is not active on this server. + /// A with a secret and code for future use or an error message. + [HttpGet("Initiate")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult Initiate([FromQuery] string? friendlyName) + { + return Ok(_quickConnect.TryConnect(friendlyName)); + } + + /// + /// Attempts to retrieve authentication information. + /// + /// Secret previously returned from the Initiate endpoint. + /// Quick connect result returned. + /// Unknown quick connect secret. + /// An updated . + [HttpGet("Connect")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult Connect([FromQuery] string? secret) + { + try + { + var result = _quickConnect.CheckRequestStatus(secret); + return Ok(result); + } + catch (ResourceNotFoundException) + { + return NotFound("Unknown secret"); + } + } + + /// + /// Temporarily activates quick connect for five minutes. + /// + /// Quick connect has been temporarily activated. + /// Quick connect is unavailable on this server. + /// An on success. + [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(); + } + + /// + /// Enables or disables quick connect. + /// + /// New . + /// Quick connect state set successfully. + /// An on success. + [HttpPost("Available")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Available([FromQuery] QuickConnectState? status) + { + _quickConnect.SetState(status ?? QuickConnectState.Available); + return NoContent(); + } + + /// + /// Authorizes a pending quick connect request. + /// + /// Quick connect code to authorize. + /// Quick connect result authorized successfully. + /// Missing quick connect code. + /// Boolean indicating if the authorization was successful. + [HttpPost("Authorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public ActionResult Authorize([FromQuery, Required] string? code) + { + if (code == null) + { + return BadRequest("Missing code"); + } + + return Ok(_quickConnect.AuthorizeRequest(Request, code)); + } + + /// + /// Deauthorize all quick connect devices for the current user. + /// + /// All quick connect devices were deleted. + /// The number of devices that were deleted. + [HttpPost("Deauthorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult 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 2723125224..131fffb7ac 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -216,6 +216,47 @@ namespace Jellyfin.Api.Controllers } } + /// + /// Authenticates a user with quick connect. + /// + /// The request. + /// User authenticated. + /// Missing token. + /// A containing an with information about the new session. + [HttpPost("AuthenticateWithQuickConnect")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> 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); + } + } + /// /// Updates a user's password. /// diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs new file mode 100644 index 0000000000..8f53d5f37f --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs @@ -0,0 +1,13 @@ +namespace Jellyfin.Api.Models.UserDtos +{ + /// + /// The quick connect request body. + /// + public class QuickConnectDto + { + /// + /// Gets or sets the quick connect token. + /// + public string? Token { get; set; } + } +} diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs deleted file mode 100644 index 7093be9908..0000000000 --- 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 - { - [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 - { - [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 - { - [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 - { - [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 - { - - } - - [Route("/QuickConnect/Available", "POST", Summary = "Enables or disables quick connect")] - [Authenticated(Roles = "Admin")] - public class Available : IReturn - { - [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 - { - - } - - public class QuickConnectService : BaseApiService - { - private IQuickConnect _quickConnect; - private IUserManager _userManager; - private IAuthorizationContext _authContext; - - public QuickConnectService( - ILogger 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 993637c8aa..fd7e973f67 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 /// HTTP request object. /// Identifying code for the request. /// A boolean indicating if the authorization completed successfully. - bool AuthorizeRequest(IRequest request, string code); + bool AuthorizeRequest(HttpRequest request, string code); /// /// Expire quick connect requests that are over the time limit. If is true, all requests are unconditionally expired. diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index ffa19fb690..d44787b885 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. /// /// The request. + /// Quick connect access token. /// Task{SessionInfo}. Task AuthenticateQuickConnect(AuthenticationRequest request, string token);