QuickConnectManager.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Security.Cryptography;
  6. using MediaBrowser.Common.Configuration;
  7. using MediaBrowser.Controller;
  8. using MediaBrowser.Controller.Configuration;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Controller.Net;
  11. using MediaBrowser.Controller.QuickConnect;
  12. using MediaBrowser.Controller.Security;
  13. using MediaBrowser.Model.Globalization;
  14. using MediaBrowser.Model.QuickConnect;
  15. using MediaBrowser.Model.Serialization;
  16. using MediaBrowser.Model.Services;
  17. using MediaBrowser.Model.Tasks;
  18. using Microsoft.Extensions.Logging;
  19. namespace Emby.Server.Implementations.QuickConnect
  20. {
  21. /// <summary>
  22. /// Quick connect implementation.
  23. /// </summary>
  24. public class QuickConnectManager : IQuickConnect
  25. {
  26. private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
  27. private Dictionary<string, QuickConnectResult> _currentRequests = new Dictionary<string, QuickConnectResult>();
  28. private IServerConfigurationManager _config;
  29. private ILogger _logger;
  30. private IUserManager _userManager;
  31. private ILocalizationManager _localizationManager;
  32. private IJsonSerializer _jsonSerializer;
  33. private IAuthenticationRepository _authenticationRepository;
  34. private IAuthorizationContext _authContext;
  35. private IServerApplicationHost _appHost;
  36. private ITaskManager _taskManager;
  37. /// <summary>
  38. /// Initializes a new instance of the <see cref="QuickConnectManager"/> class.
  39. /// Should only be called at server startup when a singleton is created.
  40. /// </summary>
  41. /// <param name="config">Configuration.</param>
  42. /// <param name="logger">Logger.</param>
  43. /// <param name="userManager">User manager.</param>
  44. /// <param name="localization">Localization.</param>
  45. /// <param name="jsonSerializer">JSON serializer.</param>
  46. /// <param name="appHost">Application host.</param>
  47. /// <param name="authContext">Authentication context.</param>
  48. /// <param name="authenticationRepository">Authentication repository.</param>
  49. /// <param name="taskManager">Task scheduler.</param>
  50. public QuickConnectManager(
  51. IServerConfigurationManager config,
  52. ILogger<QuickConnectManager> logger,
  53. IUserManager userManager,
  54. ILocalizationManager localization,
  55. IJsonSerializer jsonSerializer,
  56. IServerApplicationHost appHost,
  57. IAuthorizationContext authContext,
  58. IAuthenticationRepository authenticationRepository,
  59. ITaskManager taskManager)
  60. {
  61. _config = config;
  62. _logger = logger;
  63. _userManager = userManager;
  64. _localizationManager = localization;
  65. _jsonSerializer = jsonSerializer;
  66. _appHost = appHost;
  67. _authContext = authContext;
  68. _authenticationRepository = authenticationRepository;
  69. _taskManager = taskManager;
  70. ReloadConfiguration();
  71. }
  72. private void ReloadConfiguration()
  73. {
  74. var config = _config.GetQuickConnectConfiguration();
  75. State = config.State;
  76. }
  77. /// <inheritdoc/>
  78. public int CodeLength { get; set; } = 6;
  79. /// <inheritdoc/>
  80. public string TokenNamePrefix { get; set; } = "QuickConnect-";
  81. /// <inheritdoc/>
  82. public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
  83. /// <inheritdoc/>
  84. public int RequestExpiry { get; set; } = 30;
  85. private bool TemporaryActivation { get; set; } = false;
  86. private DateTime DateActivated { get; set; }
  87. /// <inheritdoc/>
  88. public void AssertActive()
  89. {
  90. if (State != QuickConnectState.Active)
  91. {
  92. throw new InvalidOperationException("Quick connect is not active on this server");
  93. }
  94. }
  95. /// <inheritdoc/>
  96. public QuickConnectResult Activate()
  97. {
  98. // This should not call SetEnabled since that would persist the "temporary" activation to the configuration file
  99. State = QuickConnectState.Active;
  100. DateActivated = DateTime.Now;
  101. TemporaryActivation = true;
  102. return new QuickConnectResult();
  103. }
  104. /// <inheritdoc/>
  105. public void SetEnabled(QuickConnectState newState)
  106. {
  107. _logger.LogDebug("Changed quick connect state from {0} to {1}", State, newState);
  108. State = newState;
  109. _config.SaveConfiguration("quickconnect", new QuickConnectConfiguration()
  110. {
  111. State = State
  112. });
  113. _logger.LogDebug("Configuration saved");
  114. }
  115. /// <inheritdoc/>
  116. public QuickConnectResult TryConnect(string friendlyName)
  117. {
  118. ExpireRequests();
  119. if (State != QuickConnectState.Active)
  120. {
  121. _logger.LogDebug("Refusing quick connect initiation request, current state is {0}", State);
  122. return new QuickConnectResult()
  123. {
  124. Error = "Quick connect is not active on this server"
  125. };
  126. }
  127. _logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName);
  128. var lookup = GenerateSecureRandom();
  129. var result = new QuickConnectResult()
  130. {
  131. Lookup = lookup,
  132. Secret = GenerateSecureRandom(),
  133. FriendlyName = friendlyName,
  134. DateAdded = DateTime.Now,
  135. Code = GenerateCode()
  136. };
  137. _currentRequests[lookup] = result;
  138. return result;
  139. }
  140. /// <inheritdoc/>
  141. public QuickConnectResult CheckRequestStatus(string secret)
  142. {
  143. ExpireRequests();
  144. AssertActive();
  145. string lookup = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Lookup).DefaultIfEmpty(string.Empty).First();
  146. if (!_currentRequests.ContainsKey(lookup))
  147. {
  148. throw new KeyNotFoundException("Unable to find request with provided identifier");
  149. }
  150. return _currentRequests[lookup];
  151. }
  152. /// <inheritdoc/>
  153. public List<QuickConnectResultDto> GetCurrentRequests()
  154. {
  155. return GetCurrentRequestsInternal().Select(x => (QuickConnectResultDto)x).ToList();
  156. }
  157. /// <inheritdoc/>
  158. public List<QuickConnectResult> GetCurrentRequestsInternal()
  159. {
  160. ExpireRequests();
  161. AssertActive();
  162. return _currentRequests.Values.ToList();
  163. }
  164. /// <inheritdoc/>
  165. public string GenerateCode()
  166. {
  167. int min = (int)Math.Pow(10, CodeLength - 1);
  168. int max = (int)Math.Pow(10, CodeLength);
  169. uint scale = uint.MaxValue;
  170. while (scale == uint.MaxValue)
  171. {
  172. byte[] raw = new byte[4];
  173. _rng.GetBytes(raw);
  174. scale = BitConverter.ToUInt32(raw, 0);
  175. }
  176. int code = (int)(min + (max - min) * (scale / (double)uint.MaxValue));
  177. return code.ToString(CultureInfo.InvariantCulture);
  178. }
  179. /// <inheritdoc/>
  180. public bool AuthorizeRequest(IRequest request, string lookup)
  181. {
  182. ExpireRequests();
  183. AssertActive();
  184. var auth = _authContext.GetAuthorizationInfo(request);
  185. if (!_currentRequests.ContainsKey(lookup))
  186. {
  187. throw new KeyNotFoundException("Unable to find request");
  188. }
  189. QuickConnectResult result = _currentRequests[lookup];
  190. if (result.Authenticated)
  191. {
  192. throw new InvalidOperationException("Request is already authorized");
  193. }
  194. result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
  195. // Advance the time on the request so it expires sooner as the client will pick up the changes in a few seconds
  196. var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, RequestExpiry, 0));
  197. result.DateAdded = added.Subtract(new TimeSpan(0, RequestExpiry - 1, 0));
  198. _authenticationRepository.Create(new AuthenticationInfo
  199. {
  200. AppName = TokenNamePrefix + result.FriendlyName,
  201. AccessToken = result.Authentication,
  202. DateCreated = DateTime.UtcNow,
  203. DeviceId = _appHost.SystemId,
  204. DeviceName = _appHost.FriendlyName,
  205. AppVersion = _appHost.ApplicationVersionString,
  206. UserId = auth.UserId
  207. });
  208. _logger.LogInformation("Allowing device {0} to login as user {1} with quick connect code {2}", result.FriendlyName, auth.User.Name, result.Code);
  209. return true;
  210. }
  211. /// <inheritdoc/>
  212. public int DeleteAllDevices(Guid user)
  213. {
  214. var raw = _authenticationRepository.Get(new AuthenticationInfoQuery()
  215. {
  216. DeviceId = _appHost.SystemId,
  217. UserId = user
  218. });
  219. var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture));
  220. foreach (var token in tokens)
  221. {
  222. _authenticationRepository.Delete(token);
  223. _logger.LogDebug("Deleted token {0}", token.AccessToken);
  224. }
  225. return tokens.Count();
  226. }
  227. private string GenerateSecureRandom(int length = 32)
  228. {
  229. var bytes = new byte[length];
  230. _rng.GetBytes(bytes);
  231. return string.Join(string.Empty, bytes.Select(x => x.ToString("x2", CultureInfo.InvariantCulture)));
  232. }
  233. private void ExpireRequests()
  234. {
  235. bool expireAll = false;
  236. // Check if quick connect should be deactivated
  237. if (TemporaryActivation && DateTime.Now > DateActivated.AddMinutes(10) && State == QuickConnectState.Active)
  238. {
  239. _logger.LogDebug("Quick connect time expired, deactivating");
  240. SetEnabled(QuickConnectState.Available);
  241. expireAll = true;
  242. TemporaryActivation = false;
  243. }
  244. // Expire stale connection requests
  245. var delete = new List<string>();
  246. var values = _currentRequests.Values.ToList();
  247. for (int i = 0; i < _currentRequests.Count; i++)
  248. {
  249. var added = values[i].DateAdded ?? DateTime.UnixEpoch;
  250. if (DateTime.Now > added.AddMinutes(RequestExpiry) || expireAll)
  251. {
  252. delete.Add(values[i].Lookup);
  253. }
  254. }
  255. foreach (var lookup in delete)
  256. {
  257. _logger.LogDebug("Removing expired request {0}", lookup);
  258. _currentRequests.Remove(lookup);
  259. }
  260. }
  261. }
  262. }