Apply suggestions from code review
This commit is contained in:
parent
c49a357f85
commit
5f1a863241
|
@ -3,17 +3,16 @@ using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using MediaBrowser.Common;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.QuickConnect;
|
using MediaBrowser.Controller.QuickConnect;
|
||||||
using MediaBrowser.Controller.Security;
|
using MediaBrowser.Controller.Security;
|
||||||
using MediaBrowser.Model.QuickConnect;
|
using MediaBrowser.Model.QuickConnect;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using MediaBrowser.Common;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller.Authentication;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.QuickConnect
|
namespace Emby.Server.Implementations.QuickConnect
|
||||||
{
|
{
|
||||||
|
@ -60,7 +59,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
public int CodeLength { get; set; } = 6;
|
public int CodeLength { get; set; } = 6;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string TokenNamePrefix { get; set; } = "QuickConnect-";
|
public string TokenName { get; set; } = "QuickConnect";
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
|
public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
|
||||||
|
@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Activate()
|
public void Activate()
|
||||||
{
|
{
|
||||||
DateActivated = DateTime.Now;
|
DateActivated = DateTime.UtcNow;
|
||||||
SetState(QuickConnectState.Active);
|
SetState(QuickConnectState.Active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +100,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public QuickConnectResult TryConnect(string friendlyName)
|
public QuickConnectResult TryConnect()
|
||||||
{
|
{
|
||||||
ExpireRequests();
|
ExpireRequests();
|
||||||
|
|
||||||
|
@ -111,14 +110,11 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
throw new AuthenticationException("Quick connect is not active on this server");
|
throw new AuthenticationException("Quick connect is not active on this server");
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName);
|
|
||||||
|
|
||||||
var code = GenerateCode();
|
var code = GenerateCode();
|
||||||
var result = new QuickConnectResult()
|
var result = new QuickConnectResult()
|
||||||
{
|
{
|
||||||
Secret = GenerateSecureRandom(),
|
Secret = GenerateSecureRandom(),
|
||||||
FriendlyName = friendlyName,
|
DateAdded = DateTime.UtcNow,
|
||||||
DateAdded = DateTime.Now,
|
|
||||||
Code = code
|
Code = code
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -162,13 +158,11 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool AuthorizeRequest(HttpRequest request, string code)
|
public bool AuthorizeRequest(Guid userId, string code)
|
||||||
{
|
{
|
||||||
ExpireRequests();
|
ExpireRequests();
|
||||||
AssertActive();
|
AssertActive();
|
||||||
|
|
||||||
var auth = _authContext.GetAuthorizationInfo(request);
|
|
||||||
|
|
||||||
if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
|
if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException("Unable to find request");
|
throw new ResourceNotFoundException("Unable to find request");
|
||||||
|
@ -182,21 +176,21 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
|
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
|
||||||
var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, Timeout, 0));
|
var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
|
||||||
result.DateAdded = added.Subtract(new TimeSpan(0, Timeout - 1, 0));
|
result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
|
||||||
|
|
||||||
_authenticationRepository.Create(new AuthenticationInfo
|
_authenticationRepository.Create(new AuthenticationInfo
|
||||||
{
|
{
|
||||||
AppName = TokenNamePrefix + result.FriendlyName,
|
AppName = TokenName,
|
||||||
AccessToken = result.Authentication,
|
AccessToken = result.Authentication,
|
||||||
DateCreated = DateTime.UtcNow,
|
DateCreated = DateTime.UtcNow,
|
||||||
DeviceId = _appHost.SystemId,
|
DeviceId = _appHost.SystemId,
|
||||||
DeviceName = _appHost.FriendlyName,
|
DeviceName = _appHost.FriendlyName,
|
||||||
AppVersion = _appHost.ApplicationVersionString,
|
AppVersion = _appHost.ApplicationVersionString,
|
||||||
UserId = auth.UserId
|
UserId = userId
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation("Allowing device {FriendlyName} to login as user {Username} with quick connect code {Code}", result.FriendlyName, auth.User.Username, result.Code);
|
_logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -210,7 +204,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
UserId = user
|
UserId = user
|
||||||
});
|
});
|
||||||
|
|
||||||
var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture));
|
var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.CurrentCulture));
|
||||||
|
|
||||||
var removed = 0;
|
var removed = 0;
|
||||||
foreach (var token in tokens)
|
foreach (var token in tokens)
|
||||||
|
@ -256,7 +250,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
public void ExpireRequests(bool expireAll = false)
|
public void ExpireRequests(bool expireAll = false)
|
||||||
{
|
{
|
||||||
// Check if quick connect should be deactivated
|
// Check if quick connect should be deactivated
|
||||||
if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll)
|
if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Quick connect time expired, deactivating");
|
_logger.LogDebug("Quick connect time expired, deactivating");
|
||||||
SetState(QuickConnectState.Available);
|
SetState(QuickConnectState.Available);
|
||||||
|
@ -270,7 +264,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
||||||
for (int i = 0; i < values.Count; i++)
|
for (int i = 0; i < values.Count; i++)
|
||||||
{
|
{
|
||||||
var added = values[i].DateAdded ?? DateTime.UnixEpoch;
|
var added = values[i].DateAdded ?? DateTime.UnixEpoch;
|
||||||
if (DateTime.Now > added.AddMinutes(Timeout) || expireAll)
|
if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
|
||||||
{
|
{
|
||||||
code = values[i].Code;
|
code = values[i].Code;
|
||||||
_logger.LogDebug("Removing expired request {code}", code);
|
_logger.LogDebug("Removing expired request {code}", code);
|
||||||
|
|
|
@ -1433,7 +1433,7 @@ namespace Emby.Server.Implementations.Session
|
||||||
Limit = 1
|
Limit = 1
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.TotalRecordCount < 1)
|
if (result.TotalRecordCount == 0)
|
||||||
{
|
{
|
||||||
throw new SecurityException("Unknown quick connect token");
|
throw new SecurityException("Unknown quick connect token");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.QuickConnect;
|
using MediaBrowser.Controller.QuickConnect;
|
||||||
using MediaBrowser.Model.QuickConnect;
|
using MediaBrowser.Model.QuickConnect;
|
||||||
|
@ -18,22 +18,18 @@ namespace Jellyfin.Api.Controllers
|
||||||
public class QuickConnectController : BaseJellyfinApiController
|
public class QuickConnectController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IQuickConnect _quickConnect;
|
private readonly IQuickConnect _quickConnect;
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly IAuthorizationContext _authContext;
|
private readonly IAuthorizationContext _authContext;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
|
/// Initializes a new instance of the <see cref="QuickConnectController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
|
/// <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>
|
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||||
public QuickConnectController(
|
public QuickConnectController(
|
||||||
IQuickConnect quickConnect,
|
IQuickConnect quickConnect,
|
||||||
IUserManager userManager,
|
|
||||||
IAuthorizationContext authContext)
|
IAuthorizationContext authContext)
|
||||||
{
|
{
|
||||||
_quickConnect = quickConnect;
|
_quickConnect = quickConnect;
|
||||||
_userManager = userManager;
|
|
||||||
_authContext = authContext;
|
_authContext = authContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,15 +49,14 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initiate a new quick connect request.
|
/// Initiate a new quick connect request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="friendlyName">Device friendly name.</param>
|
|
||||||
/// <response code="200">Quick connect request successfully created.</response>
|
/// <response code="200">Quick connect request successfully created.</response>
|
||||||
/// <response code="401">Quick connect is not active on this server.</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>
|
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
|
||||||
[HttpGet("Initiate")]
|
[HttpGet("Initiate")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QuickConnectResult> Initiate([FromQuery] string? friendlyName)
|
public ActionResult<QuickConnectResult> Initiate()
|
||||||
{
|
{
|
||||||
return _quickConnect.TryConnect(friendlyName);
|
return _quickConnect.TryConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -74,12 +69,11 @@ namespace Jellyfin.Api.Controllers
|
||||||
[HttpGet("Connect")]
|
[HttpGet("Connect")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult<QuickConnectResult> Connect([FromQuery] string? secret)
|
public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _quickConnect.CheckRequestStatus(secret);
|
return _quickConnect.CheckRequestStatus(secret);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
catch (ResourceNotFoundException)
|
catch (ResourceNotFoundException)
|
||||||
{
|
{
|
||||||
|
@ -117,9 +111,9 @@ namespace Jellyfin.Api.Controllers
|
||||||
[HttpPost("Available")]
|
[HttpPost("Available")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult Available([FromQuery] QuickConnectState? status)
|
public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
|
||||||
{
|
{
|
||||||
_quickConnect.SetState(status ?? QuickConnectState.Available);
|
_quickConnect.SetState(status);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,16 +121,22 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// Authorizes a pending quick connect request.
|
/// Authorizes a pending quick connect request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code">Quick connect code to authorize.</param>
|
/// <param name="code">Quick connect code to authorize.</param>
|
||||||
|
/// <param name="userId">User id.</param>
|
||||||
/// <response code="200">Quick connect result authorized successfully.</response>
|
/// <response code="200">Quick connect result authorized successfully.</response>
|
||||||
/// <response code="400">Missing quick connect code.</response>
|
/// <response code="403">User is not allowed to authorize quick connect requests.</response>
|
||||||
/// <returns>Boolean indicating if the authorization was successful.</returns>
|
/// <returns>Boolean indicating if the authorization was successful.</returns>
|
||||||
[HttpPost("Authorize")]
|
[HttpPost("Authorize")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult<bool> Authorize([FromQuery, Required] string? code)
|
public ActionResult<bool> Authorize([FromQuery, Required] string code, [FromQuery, Required] Guid userId)
|
||||||
{
|
{
|
||||||
return _quickConnect.AuthorizeRequest(Request, code);
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
|
{
|
||||||
|
return Forbid("User is not allowed to authorize quick connect requests.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _quickConnect.AuthorizeRequest(userId, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -239,11 +239,9 @@ namespace Jellyfin.Api.Controllers
|
||||||
DeviceName = auth.Device,
|
DeviceName = auth.Device,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _sessionManager.AuthenticateQuickConnect(
|
return await _sessionManager.AuthenticateQuickConnect(
|
||||||
authRequest,
|
authRequest,
|
||||||
request.Token).ConfigureAwait(false);
|
request.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
catch (SecurityException e)
|
catch (SecurityException e)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using MediaBrowser.Model.QuickConnect;
|
using MediaBrowser.Model.QuickConnect;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.QuickConnect
|
namespace MediaBrowser.Controller.QuickConnect
|
||||||
{
|
{
|
||||||
|
@ -15,9 +14,9 @@ namespace MediaBrowser.Controller.QuickConnect
|
||||||
int CodeLength { get; set; }
|
int CodeLength { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the string to prefix internal access tokens with.
|
/// Gets or sets the name of internal access tokens.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string TokenNamePrefix { get; set; }
|
string TokenName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current state of quick connect.
|
/// Gets the current state of quick connect.
|
||||||
|
@ -48,9 +47,8 @@ namespace MediaBrowser.Controller.QuickConnect
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initiates a new quick connect request.
|
/// Initiates a new quick connect request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="friendlyName">Friendly device name to display in the request UI.</param>
|
|
||||||
/// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
|
/// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
|
||||||
QuickConnectResult TryConnect(string friendlyName);
|
QuickConnectResult TryConnect();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks the status of an individual request.
|
/// Checks the status of an individual request.
|
||||||
|
@ -62,10 +60,10 @@ namespace MediaBrowser.Controller.QuickConnect
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authorizes a quick connect request to connect as the calling user.
|
/// Authorizes a quick connect request to connect as the calling user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">HTTP request object.</param>
|
/// <param name="userId">User id.</param>
|
||||||
/// <param name="code">Identifying code for the request.</param>
|
/// <param name="code">Identifying code for the request.</param>
|
||||||
/// <returns>A boolean indicating if the authorization completed successfully.</returns>
|
/// <returns>A boolean indicating if the authorization completed successfully.</returns>
|
||||||
bool AuthorizeRequest(HttpRequest request, string code);
|
bool AuthorizeRequest(Guid userId, string code);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.
|
/// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.
|
||||||
|
|
|
@ -22,11 +22,6 @@ namespace MediaBrowser.Model.QuickConnect
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Code { get; set; }
|
public string? Code { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the device friendly name.
|
|
||||||
/// </summary>
|
|
||||||
public string? FriendlyName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the private access token.
|
/// Gets or sets the private access token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user