QuickConnectManager.cs 11 KB

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