diff options
| -rw-r--r-- | CONTRIBUTORS.md | 1 | ||||
| -rw-r--r-- | Emby.Server.Implementations/ApplicationHost.cs | 4 | ||||
| -rw-r--r-- | Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs | 28 | ||||
| -rw-r--r-- | Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs | 13 | ||||
| -rw-r--r-- | Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs | 315 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Session/SessionManager.cs | 18 | ||||
| -rw-r--r-- | MediaBrowser.Api/QuickConnect/QuickConnectService.cs | 175 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserService.cs | 34 | ||||
| -rw-r--r-- | MediaBrowser.Controller/QuickConnect/IQuickConnect.cs | 97 | ||||
| -rw-r--r-- | MediaBrowser.Controller/Session/ISessionManager.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Model/QuickConnect/QuickConnectResult.cs | 50 | ||||
| -rw-r--r-- | MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs | 53 | ||||
| -rw-r--r-- | MediaBrowser.Model/QuickConnect/QuickConnectState.cs | 23 |
13 files changed, 813 insertions, 0 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ce956176e..edd33a2fb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -15,6 +15,7 @@ - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) - [ckcr4lyf](https://github.com/ckcr4lyf) + - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [crankdoofus](https://github.com/crankdoofus) - [crobibero](https://github.com/crobibero) - [cromefire](https://github.com/cromefire) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index be4e05a64..0a349bb33 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -73,6 +73,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -104,6 +105,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using Emby.Server.Implementations.QuickConnect; namespace Emby.Server.Implementations { @@ -634,6 +636,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISessionContext, SessionContext>(); serviceCollection.AddSingleton<IAuthService, AuthService>(); + serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); @@ -643,6 +646,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); } + /// <summary> /// Create services registered with the service container that need to be initialized at application startup. /// </summary> diff --git a/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs new file mode 100644 index 000000000..458bb7614 --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/ConfigurationExtension.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Emby.Server.Implementations.QuickConnect +{ + public static class ConfigurationExtension + { + public static QuickConnectConfiguration GetQuickConnectConfiguration(this IConfigurationManager manager) + { + return manager.GetConfiguration<QuickConnectConfiguration>("quickconnect"); + } + } + + public class QuickConnectConfigurationFactory : IConfigurationFactory + { + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new ConfigurationStore[] + { + new ConfigurationStore + { + Key = "quickconnect", + ConfigurationType = typeof(QuickConnectConfiguration) + } + }; + } + } +} diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs new file mode 100644 index 000000000..befc46379 --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectConfiguration.cs @@ -0,0 +1,13 @@ +using MediaBrowser.Model.QuickConnect; + +namespace Emby.Server.Implementations.QuickConnect +{ + public class QuickConnectConfiguration + { + public QuickConnectConfiguration() + { + } + + public QuickConnectState State { get; set; } + } +} diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs new file mode 100644 index 000000000..b8b51adb6 --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.QuickConnect; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.QuickConnect +{ + /// <summary> + /// Quick connect implementation. + /// </summary> + public class QuickConnectManager : IQuickConnect + { + private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + private Dictionary<string, QuickConnectResult> _currentRequests = new Dictionary<string, QuickConnectResult>(); + + private IServerConfigurationManager _config; + private ILogger _logger; + private IUserManager _userManager; + private ILocalizationManager _localizationManager; + private IJsonSerializer _jsonSerializer; + private IAuthenticationRepository _authenticationRepository; + private IAuthorizationContext _authContext; + private IServerApplicationHost _appHost; + private ITaskManager _taskManager; + + /// <summary> + /// Initializes a new instance of the <see cref="QuickConnectManager"/> class. + /// Should only be called at server startup when a singleton is created. + /// </summary> + /// <param name="config">Configuration.</param> + /// <param name="logger">Logger.</param> + /// <param name="userManager">User manager.</param> + /// <param name="localization">Localization.</param> + /// <param name="jsonSerializer">JSON serializer.</param> + /// <param name="appHost">Application host.</param> + /// <param name="authContext">Authentication context.</param> + /// <param name="authenticationRepository">Authentication repository.</param> + /// <param name="taskManager">Task scheduler.</param> + public QuickConnectManager( + IServerConfigurationManager config, + ILogger<QuickConnectManager> logger, + IUserManager userManager, + ILocalizationManager localization, + IJsonSerializer jsonSerializer, + IServerApplicationHost appHost, + IAuthorizationContext authContext, + IAuthenticationRepository authenticationRepository, + ITaskManager taskManager) + { + _config = config; + _logger = logger; + _userManager = userManager; + _localizationManager = localization; + _jsonSerializer = jsonSerializer; + _appHost = appHost; + _authContext = authContext; + _authenticationRepository = authenticationRepository; + _taskManager = taskManager; + + ReloadConfiguration(); + } + + private void ReloadConfiguration() + { + var config = _config.GetQuickConnectConfiguration(); + + State = config.State; + } + + /// <inheritdoc/> + public int CodeLength { get; set; } = 6; + + /// <inheritdoc/> + public string TokenNamePrefix { get; set; } = "QuickConnect-"; + + /// <inheritdoc/> + public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; + + /// <inheritdoc/> + public int RequestExpiry { get; set; } = 30; + + private bool TemporaryActivation { get; set; } = false; + + private DateTime DateActivated { get; set; } + + /// <inheritdoc/> + public void AssertActive() + { + if (State != QuickConnectState.Active) + { + throw new InvalidOperationException("Quick connect is not active on this server"); + } + } + + /// <inheritdoc/> + public QuickConnectResult Activate() + { + // This should not call SetEnabled since that would persist the "temporary" activation to the configuration file + State = QuickConnectState.Active; + DateActivated = DateTime.Now; + TemporaryActivation = true; + + return new QuickConnectResult(); + } + + /// <inheritdoc/> + public void SetEnabled(QuickConnectState newState) + { + _logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState); + + State = newState; + + _config.SaveConfiguration("quickconnect", new QuickConnectConfiguration() + { + State = State + }); + + _logger.LogDebug("Configuration saved"); + } + + /// <inheritdoc/> + public QuickConnectResult TryConnect(string friendlyName) + { + ExpireRequests(); + + if (State != QuickConnectState.Active) + { + _logger.LogDebug("Refusing quick connect initiation request, current state is {0}", State); + + return new QuickConnectResult() + { + Error = "Quick connect is not active on this server" + }; + } + + _logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName); + + var lookup = GenerateSecureRandom(); + var result = new QuickConnectResult() + { + Lookup = lookup, + Secret = GenerateSecureRandom(), + FriendlyName = friendlyName, + DateAdded = DateTime.Now, + Code = GenerateCode() + }; + + _currentRequests[lookup] = result; + return result; + } + + /// <inheritdoc/> + public QuickConnectResult CheckRequestStatus(string secret) + { + ExpireRequests(); + AssertActive(); + + string lookup = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Lookup).DefaultIfEmpty(string.Empty).First(); + + if (!_currentRequests.ContainsKey(lookup)) + { + throw new KeyNotFoundException("Unable to find request with provided identifier"); + } + + return _currentRequests[lookup]; + } + + /// <inheritdoc/> + public List<QuickConnectResultDto> GetCurrentRequests() + { + return GetCurrentRequestsInternal().Select(x => (QuickConnectResultDto)x).ToList(); + } + + /// <inheritdoc/> + public List<QuickConnectResult> GetCurrentRequestsInternal() + { + ExpireRequests(); + AssertActive(); + return _currentRequests.Values.ToList(); + } + + /// <inheritdoc/> + public string GenerateCode() + { + int min = (int)Math.Pow(10, CodeLength - 1); + int max = (int)Math.Pow(10, CodeLength); + + uint scale = uint.MaxValue; + while (scale == uint.MaxValue) + { + byte[] raw = new byte[4]; + _rng.GetBytes(raw); + scale = BitConverter.ToUInt32(raw, 0); + } + + int code = (int)(min + (max - min) * (scale / (double)uint.MaxValue)); + return code.ToString(CultureInfo.InvariantCulture); + } + + /// <inheritdoc/> + public bool AuthorizeRequest(IRequest request, string lookup) + { + ExpireRequests(); + AssertActive(); + + var auth = _authContext.GetAuthorizationInfo(request); + + if (!_currentRequests.ContainsKey(lookup)) + { + throw new KeyNotFoundException("Unable to find request"); + } + + QuickConnectResult result = _currentRequests[lookup]; + + if (result.Authenticated) + { + throw new InvalidOperationException("Request is already authorized"); + } + + result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + + // Advance the time on the request so it expires sooner as the client will pick up the changes in a few seconds + result.DateAdded = result.DateAdded.Subtract(new TimeSpan(0, RequestExpiry - 1, 0)); + + _authenticationRepository.Create(new AuthenticationInfo + { + AppName = TokenNamePrefix + result.FriendlyName, + AccessToken = result.Authentication, + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString, + UserId = auth.UserId + }); + + _logger.LogInformation("Allowing device {0} to login as user {1} with quick connect code {2}", result.FriendlyName, auth.User.Name, result.Code); + + return true; + } + + /// <inheritdoc/> + public int DeleteAllDevices(Guid user) + { + var raw = _authenticationRepository.Get(new AuthenticationInfoQuery() + { + DeviceId = _appHost.SystemId, + UserId = user + }); + + var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture)); + + foreach (var token in tokens) + { + _authenticationRepository.Delete(token); + _logger.LogDebug("Deleted token {0}", token.AccessToken); + } + + return tokens.Count(); + } + + private string GenerateSecureRandom(int length = 32) + { + var bytes = new byte[length]; + _rng.GetBytes(bytes); + + return string.Join(string.Empty, bytes.Select(x => x.ToString("x2", CultureInfo.InvariantCulture))); + } + + private void ExpireRequests() + { + bool expireAll = false; + + // check if quick connect should be deactivated + if (TemporaryActivation && DateTime.Now > DateActivated.AddMinutes(10) && State == QuickConnectState.Active) + { + _logger.LogDebug("Quick connect time expired, deactivating"); + SetEnabled(QuickConnectState.Available); + expireAll = true; + TemporaryActivation = false; + } + + // expire stale connection requests + var delete = new List<string>(); + var values = _currentRequests.Values.ToList(); + + for (int i = 0; i < _currentRequests.Count; i++) + { + if (DateTime.Now > values[i].DateAdded.AddMinutes(RequestExpiry) || expireAll) + { + delete.Add(values[i].Lookup); + } + } + + foreach (var lookup in delete) + { + _logger.LogDebug("Removing expired request {0}", lookup); + _currentRequests.Remove(lookup); + } + } + } +} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 5c480e842..d7054e0b1 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1404,6 +1404,24 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, false); } + public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token) + { + var result = _authRepo.Get(new AuthenticationInfoQuery() + { + AccessToken = token, + DeviceId = _appHost.SystemId, + Limit = 1 + }); + + if(result.TotalRecordCount < 1) + { + throw new SecurityException("Unknown quick connect token"); + } + + request.UserId = result.Items[0].UserId; + return AuthenticateNewSessionInternal(request, false); + } + private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { CheckDisposed(); diff --git a/MediaBrowser.Api/QuickConnect/QuickConnectService.cs b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs new file mode 100644 index 000000000..60d6ac414 --- /dev/null +++ b/MediaBrowser.Api/QuickConnect/QuickConnectService.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +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/List", "GET", Summary = "Lists all quick connect requests")] + [Authenticated] + public class QuickConnectList : IReturn<List<QuickConnectResultDto>> + { + } + + [Route("/QuickConnect/Authorize", "POST", Summary = "Authorizes a pending quick connect request")] + [Authenticated] + public class Authorize : IReturn<QuickConnectResultDto> + { + [ApiMember(Name = "Lookup", Description = "Quick connect public lookup", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Lookup { 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<QuickConnectState> + { + } + + 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(QuickConnectList request) + { + if(_quickConnect.State != QuickConnectState.Active) + { + return Array.Empty<QuickConnectResultDto>(); + } + + return _quickConnect.GetCurrentRequests(); + } + + public object Get(QuickConnectStatus request) + { + return _quickConnect.State; + } + + public object Post(Deauthorize request) + { + AssertCanUpdateUser(_authContext, _userManager, request.UserId, true); + + return _quickConnect.DeleteAllDevices(request.UserId); + } + + public object Post(Authorize request) + { + bool result = _quickConnect.AuthorizeRequest(Request, request.Lookup); + + Logger.LogInformation("Result of authorizing quick connect {0}: {1}", request.Lookup[..10], result); + + return result; + } + + public object Post(Activate request) + { + string name = _authContext.GetAuthorizationInfo(Request).User.Name; + + if(_quickConnect.State == QuickConnectState.Unavailable) + { + return new QuickConnectResult() + { + Error = "Quick connect is not enabled on this server" + }; + } + + else if(_quickConnect.State == QuickConnectState.Available) + { + var result = _quickConnect.Activate(); + + if (string.IsNullOrEmpty(result.Error)) + { + Logger.LogInformation("{name} temporarily activated quick connect", name); + } + + return result; + } + + else if(_quickConnect.State == QuickConnectState.Active) + { + return new QuickConnectResult() + { + Error = "" + }; + } + + return new QuickConnectResult() + { + Error = "Unknown current state" + }; + } + + public object Post(Available request) + { + _quickConnect.SetEnabled(request.Status); + + return _quickConnect.State; + } + } +} diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 78fc6c694..98eeda544 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -117,6 +117,17 @@ namespace MediaBrowser.Api public string Pw { get; set; } } + [Route("/Users/AuthenticateWithQuickConnect", "POST", Summary = "Authenticates a user")] + public class AuthenticateUserQuickConnect : IReturn<AuthenticationResult> + { + /// <summary> + /// Gets or sets the token. + /// </summary> + /// <value>The token</value> + [ApiMember(Name = "Token", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")] + public string Token { get; set; } + } + /// <summary> /// Class UpdateUserPassword /// </summary> @@ -430,6 +441,29 @@ namespace MediaBrowser.Api } } + public async Task<object> Post(AuthenticateUserQuickConnect request) + { + var auth = _authContext.GetAuthorizationInfo(Request); + + try + { + var result = await _sessionMananger.AuthenticateQuickConnect(new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device + }, request.Token).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{Request.RemoteIp}] {e.Message}"); + } + } + /// <summary> /// Posts the specified request. /// </summary> diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs new file mode 100644 index 000000000..d44765e11 --- /dev/null +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.QuickConnect; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Controller.QuickConnect +{ + /// <summary> + /// Quick connect standard interface. + /// </summary> + public interface IQuickConnect + { + /// <summary> + /// Gets or sets the length of user facing codes. + /// </summary> + public int CodeLength { get; set; } + + /// <summary> + /// Gets or sets the string to prefix internal access tokens with. + /// </summary> + public string TokenNamePrefix { get; set; } + + /// <summary> + /// Gets the current state of quick connect. + /// </summary> + public QuickConnectState State { get; } + + /// <summary> + /// Gets or sets the time (in minutes) before a pending request will expire. + /// </summary> + public int RequestExpiry { get; set; } + + /// <summary> + /// Assert that quick connect is currently active and throws an exception if it is not. + /// </summary> + void AssertActive(); + + /// <summary> + /// Temporarily activates quick connect for a short amount of time. + /// </summary> + /// <returns>A quick connect result object indicating success.</returns> + QuickConnectResult Activate(); + + /// <summary> + /// Changes the status of quick connect. + /// </summary> + /// <param name="newState">New state to change to.</param> + void SetEnabled(QuickConnectState newState); + + /// <summary> + /// Initiates a new quick connect request. + /// </summary> + /// <param name="friendlyName">Friendly device name to display in the request UI.</param> + /// <returns>A quick connect result with tokens to proceed or a descriptive error message otherwise.</returns> + QuickConnectResult TryConnect(string friendlyName); + + /// <summary> + /// Checks the status of an individual request. + /// </summary> + /// <param name="secret">Unique secret identifier of the request.</param> + /// <returns>Quick connect result.</returns> + QuickConnectResult CheckRequestStatus(string secret); + + /// <summary> + /// Returns all current quick connect requests as DTOs. Does not include sensitive information. + /// </summary> + /// <returns>List of all quick connect results.</returns> + List<QuickConnectResultDto> GetCurrentRequests(); + + /// <summary> + /// Returns all current quick connect requests (including sensitive information). + /// </summary> + /// <returns>List of all quick connect results.</returns> + List<QuickConnectResult> GetCurrentRequestsInternal(); + + /// <summary> + /// Authorizes a quick connect request to connect as the calling user. + /// </summary> + /// <param name="request">HTTP request object.</param> + /// <param name="lookup">Public request lookup value.</param> + /// <returns>A boolean indicating if the authorization completed successfully.</returns> + bool AuthorizeRequest(IRequest request, string lookup); + + /// <summary> + /// Deletes all quick connect access tokens for the provided user. + /// </summary> + /// <param name="user">Guid of the user to delete tokens for.</param> + /// <returns>A count of the deleted tokens.</returns> + int DeleteAllDevices(Guid user); + + /// <summary> + /// Generates a short code to display to the user to uniquely identify this request. + /// </summary> + /// <returns>A short, unique alphanumeric string.</returns> + string GenerateCode(); + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 4c2f834cb..efda51d4b 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -265,6 +265,8 @@ namespace MediaBrowser.Controller.Session /// <returns>Task{SessionInfo}.</returns> Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request); + public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token); + /// <summary> /// Creates the new session. /// </summary> diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs new file mode 100644 index 000000000..bc3fd0046 --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs @@ -0,0 +1,50 @@ +using System; + +namespace MediaBrowser.Model.QuickConnect +{ + /// <summary> + /// Stores the result of an incoming quick connect request. + /// </summary> + public class QuickConnectResult + { + /// <summary> + /// Gets a value indicating whether this request is authorized. + /// </summary> + public bool Authenticated => !string.IsNullOrEmpty(Authentication); + + /// <summary> + /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information. + /// </summary> + public string Secret { get; set; } + + /// <summary> + /// Gets or sets the public value used to uniquely identify this request. Can only be used to authorize the request. + /// </summary> + public string Lookup { get; set; } + + /// <summary> + /// Gets or sets the user facing code used so the user can quickly differentiate this request from others. + /// </summary> + public string Code { get; set; } + + /// <summary> + /// Gets or sets the device friendly name. + /// </summary> + public string FriendlyName { get; set; } + + /// <summary> + /// Gets or sets the private access token. + /// </summary> + public string Authentication { get; set; } + + /// <summary> + /// Gets or sets an error message. + /// </summary> + public string Error { get; set; } + + /// <summary> + /// Gets or sets the DateTime that this request was created. + /// </summary> + public DateTime DateAdded { get; set; } + } +} diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs new file mode 100644 index 000000000..671b7cc94 --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResultDto.cs @@ -0,0 +1,53 @@ +using System; + +namespace MediaBrowser.Model.QuickConnect +{ + /// <summary> + /// Stores the non-sensitive results of an incoming quick connect request. + /// </summary> + public class QuickConnectResultDto + { + /// <summary> + /// Gets a value indicating whether this request is authorized. + /// </summary> + public bool Authenticated { get; private set; } + + /// <summary> + /// Gets the user facing code used so the user can quickly differentiate this request from others. + /// </summary> + public string Code { get; private set; } + + /// <summary> + /// Gets the public value used to uniquely identify this request. Can only be used to authorize the request. + /// </summary> + public string Lookup { get; private set; } + + /// <summary> + /// Gets the device friendly name. + /// </summary> + public string FriendlyName { get; private set; } + + /// <summary> + /// Gets the DateTime that this request was created. + /// </summary> + public DateTime DateAdded { get; private set; } + + /// <summary> + /// Cast an internal quick connect result to a DTO by removing all sensitive properties. + /// </summary> + /// <param name="result">QuickConnectResult object to cast</param> + public static implicit operator QuickConnectResultDto(QuickConnectResult result) + { + QuickConnectResultDto resultDto = new QuickConnectResultDto + { + Authenticated = result.Authenticated, + Code = result.Code, + FriendlyName = result.FriendlyName, + DateAdded = result.DateAdded, + Lookup = result.Lookup + }; + + return resultDto; + } + } +} diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs new file mode 100644 index 000000000..f1074f25f --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.QuickConnect +{ + /// <summary> + /// Quick connect state. + /// </summary> + public enum QuickConnectState + { + /// <summary> + /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in. + /// </summary> + Unavailable = 0, + + /// <summary> + /// The feature is enabled for use on the server but is not currently accepting connection requests. + /// </summary> + Available = 1, + + /// <summary> + /// The feature is actively accepting connection requests. + /// </summary> + Active = 2 + } +} |
