|
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|
|
using MediaBrowser.Common.Extensions;
|
|
|
using MediaBrowser.Controller.Authentication;
|
|
|
using MediaBrowser.Controller.Configuration;
|
|
|
+using MediaBrowser.Controller.Net;
|
|
|
using MediaBrowser.Controller.QuickConnect;
|
|
|
using MediaBrowser.Controller.Session;
|
|
|
using MediaBrowser.Model.QuickConnect;
|
|
@@ -29,8 +30,9 @@ namespace Emby.Server.Implementations.QuickConnect
|
|
|
/// </summary>
|
|
|
private const int Timeout = 10;
|
|
|
|
|
|
- private readonly RNGCryptoServiceProvider _rng = new();
|
|
|
- private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new();
|
|
|
+ private readonly RNGCryptoServiceProvider _rng = new ();
|
|
|
+ private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ();
|
|
|
+ private readonly ConcurrentDictionary<string, (DateTime Timestamp, AuthenticationResult AuthenticationResult)> _authorizedSecrets = new ();
|
|
|
|
|
|
private readonly IServerConfigurationManager _config;
|
|
|
private readonly ILogger<QuickConnectManager> _logger;
|
|
@@ -68,14 +70,41 @@ namespace Emby.Server.Implementations.QuickConnect
|
|
|
}
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
- public QuickConnectResult TryConnect()
|
|
|
+ public QuickConnectResult TryConnect(AuthorizationInfo authorizationInfo)
|
|
|
{
|
|
|
+ if (string.IsNullOrEmpty(authorizationInfo.DeviceId))
|
|
|
+ {
|
|
|
+ throw new ArgumentException(nameof(authorizationInfo.DeviceId) + " is required");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(authorizationInfo.Device))
|
|
|
+ {
|
|
|
+ throw new ArgumentException(nameof(authorizationInfo.Device) + " is required");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(authorizationInfo.Client))
|
|
|
+ {
|
|
|
+ throw new ArgumentException(nameof(authorizationInfo.Client) + " is required");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.IsNullOrEmpty(authorizationInfo.Version))
|
|
|
+ {
|
|
|
+ throw new ArgumentException(nameof(authorizationInfo.Version) + "is required");
|
|
|
+ }
|
|
|
+
|
|
|
AssertActive();
|
|
|
ExpireRequests();
|
|
|
|
|
|
var secret = GenerateSecureRandom();
|
|
|
var code = GenerateCode();
|
|
|
- var result = new QuickConnectResult(secret, code, DateTime.UtcNow);
|
|
|
+ var result = new QuickConnectResult(
|
|
|
+ secret,
|
|
|
+ code,
|
|
|
+ DateTime.UtcNow,
|
|
|
+ authorizationInfo.DeviceId,
|
|
|
+ authorizationInfo.Device,
|
|
|
+ authorizationInfo.Client,
|
|
|
+ authorizationInfo.Version);
|
|
|
|
|
|
_currentRequests[code] = result;
|
|
|
return result;
|
|
@@ -135,19 +164,41 @@ namespace Emby.Server.Implementations.QuickConnect
|
|
|
throw new InvalidOperationException("Request is already authorized");
|
|
|
}
|
|
|
|
|
|
- var token = Guid.NewGuid();
|
|
|
- result.Authentication = token;
|
|
|
-
|
|
|
// 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.
|
|
|
- result.DateAdded = DateTime.Now.Add(TimeSpan.FromMinutes(1));
|
|
|
+ result.DateAdded = DateTime.UtcNow.Add(TimeSpan.FromMinutes(1));
|
|
|
|
|
|
- await _sessionManager.AuthenticateQuickConnect(userId).ConfigureAwait(false);
|
|
|
+ var authenticationResult = await _sessionManager.AuthenticateDirect(new AuthenticationRequest
|
|
|
+ {
|
|
|
+ UserId = userId,
|
|
|
+ DeviceId = result.DeviceId,
|
|
|
+ DeviceName = result.DeviceName,
|
|
|
+ App = result.AppName,
|
|
|
+ AppVersion = result.AppVersion
|
|
|
+ }).ConfigureAwait(false);
|
|
|
+
|
|
|
+ _authorizedSecrets[result.Secret] = (DateTime.UtcNow, authenticationResult);
|
|
|
+ result.Authenticated = true;
|
|
|
+ _currentRequests[code] = result;
|
|
|
|
|
|
- _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
|
|
|
+ _logger.LogDebug("Authorizing device with code {Code} to login as user {UserId}", code, userId);
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ /// <inheritdoc/>
|
|
|
+ public AuthenticationResult GetAuthorizedRequest(string secret)
|
|
|
+ {
|
|
|
+ AssertActive();
|
|
|
+ ExpireRequests();
|
|
|
+
|
|
|
+ if (!_authorizedSecrets.TryGetValue(secret, out var result))
|
|
|
+ {
|
|
|
+ throw new ResourceNotFoundException("Unable to find request");
|
|
|
+ }
|
|
|
+
|
|
|
+ return result.AuthenticationResult;
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Dispose.
|
|
|
/// </summary>
|
|
@@ -189,7 +240,7 @@ namespace Emby.Server.Implementations.QuickConnect
|
|
|
// Expire stale connection requests
|
|
|
foreach (var (_, currentRequest) in _currentRequests)
|
|
|
{
|
|
|
- if (expireAll || currentRequest.DateAdded > minTime)
|
|
|
+ if (expireAll || currentRequest.DateAdded < minTime)
|
|
|
{
|
|
|
var code = currentRequest.Code;
|
|
|
_logger.LogDebug("Removing expired request {Code}", code);
|
|
@@ -200,6 +251,18 @@ namespace Emby.Server.Implementations.QuickConnect
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ foreach (var (secret, (timestamp, _)) in _authorizedSecrets)
|
|
|
+ {
|
|
|
+ if (expireAll || timestamp < minTime)
|
|
|
+ {
|
|
|
+ _logger.LogDebug("Removing expired secret {Secret}", secret);
|
|
|
+ if (!_authorizedSecrets.TryRemove(secret, out _))
|
|
|
+ {
|
|
|
+ _logger.LogWarning("Secret {Secret} already expired", secret);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|