ConnectManager.cs 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Net;
  3. using MediaBrowser.Controller;
  4. using MediaBrowser.Controller.Configuration;
  5. using MediaBrowser.Controller.Connect;
  6. using MediaBrowser.Controller.Entities;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Controller.Providers;
  9. using MediaBrowser.Controller.Security;
  10. using MediaBrowser.Model.Connect;
  11. using MediaBrowser.Model.Entities;
  12. using MediaBrowser.Model.Events;
  13. using MediaBrowser.Model.Logging;
  14. using MediaBrowser.Model.Net;
  15. using MediaBrowser.Model.Serialization;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Globalization;
  19. using System.IO;
  20. using System.Linq;
  21. using System.Net;
  22. using System.Text;
  23. using System.Threading;
  24. using System.Threading.Tasks;
  25. namespace MediaBrowser.Server.Implementations.Connect
  26. {
  27. public class ConnectManager : IConnectManager
  28. {
  29. private readonly SemaphoreSlim _operationLock = new SemaphoreSlim(1, 1);
  30. private readonly ILogger _logger;
  31. private readonly IApplicationPaths _appPaths;
  32. private readonly IJsonSerializer _json;
  33. private readonly IEncryptionManager _encryption;
  34. private readonly IHttpClient _httpClient;
  35. private readonly IServerApplicationHost _appHost;
  36. private readonly IServerConfigurationManager _config;
  37. private readonly IUserManager _userManager;
  38. private readonly IProviderManager _providerManager;
  39. private ConnectData _data = new ConnectData();
  40. public string ConnectServerId
  41. {
  42. get { return _data.ServerId; }
  43. }
  44. public string ConnectAccessKey
  45. {
  46. get { return _data.AccessKey; }
  47. }
  48. public string DiscoveredWanIpAddress { get; private set; }
  49. public string WanIpAddress
  50. {
  51. get
  52. {
  53. var address = _config.Configuration.WanDdns;
  54. if (string.IsNullOrWhiteSpace(address))
  55. {
  56. address = DiscoveredWanIpAddress;
  57. }
  58. return address;
  59. }
  60. }
  61. public string WanApiAddress
  62. {
  63. get
  64. {
  65. var ip = WanIpAddress;
  66. if (!string.IsNullOrEmpty(ip))
  67. {
  68. if (!ip.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
  69. !ip.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
  70. {
  71. ip = "http://" + ip;
  72. }
  73. return ip + ":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture);
  74. }
  75. return null;
  76. }
  77. }
  78. public ConnectManager(ILogger logger,
  79. IApplicationPaths appPaths,
  80. IJsonSerializer json,
  81. IEncryptionManager encryption,
  82. IHttpClient httpClient,
  83. IServerApplicationHost appHost,
  84. IServerConfigurationManager config, IUserManager userManager, IProviderManager providerManager)
  85. {
  86. _logger = logger;
  87. _appPaths = appPaths;
  88. _json = json;
  89. _encryption = encryption;
  90. _httpClient = httpClient;
  91. _appHost = appHost;
  92. _config = config;
  93. _userManager = userManager;
  94. _providerManager = providerManager;
  95. _userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated;
  96. LoadCachedData();
  97. }
  98. internal void OnWanAddressResolved(string address)
  99. {
  100. DiscoveredWanIpAddress = address;
  101. UpdateConnectInfo();
  102. }
  103. private async void UpdateConnectInfo()
  104. {
  105. await _operationLock.WaitAsync().ConfigureAwait(false);
  106. try
  107. {
  108. await UpdateConnectInfoInternal().ConfigureAwait(false);
  109. }
  110. finally
  111. {
  112. _operationLock.Release();
  113. }
  114. }
  115. private async Task UpdateConnectInfoInternal()
  116. {
  117. var wanApiAddress = WanApiAddress;
  118. if (string.IsNullOrWhiteSpace(wanApiAddress))
  119. {
  120. _logger.Warn("Cannot update Media Browser Connect information without a WanApiAddress");
  121. return;
  122. }
  123. try
  124. {
  125. var localAddress = _appHost.GetSystemInfo().LocalAddress;
  126. var hasExistingRecord = !string.IsNullOrWhiteSpace(ConnectServerId) &&
  127. !string.IsNullOrWhiteSpace(ConnectAccessKey);
  128. var createNewRegistration = !hasExistingRecord;
  129. if (hasExistingRecord)
  130. {
  131. try
  132. {
  133. await UpdateServerRegistration(wanApiAddress, localAddress).ConfigureAwait(false);
  134. }
  135. catch (HttpException ex)
  136. {
  137. if (!ex.StatusCode.HasValue ||
  138. !new[] { HttpStatusCode.NotFound, HttpStatusCode.Unauthorized }.Contains(ex.StatusCode.Value))
  139. {
  140. throw;
  141. }
  142. createNewRegistration = true;
  143. }
  144. }
  145. if (createNewRegistration)
  146. {
  147. await CreateServerRegistration(wanApiAddress, localAddress).ConfigureAwait(false);
  148. }
  149. await RefreshAuthorizationsInternal(true, CancellationToken.None).ConfigureAwait(false);
  150. }
  151. catch (Exception ex)
  152. {
  153. _logger.ErrorException("Error registering with Connect", ex);
  154. }
  155. }
  156. private async Task CreateServerRegistration(string wanApiAddress, string localAddress)
  157. {
  158. if (string.IsNullOrWhiteSpace(wanApiAddress))
  159. {
  160. throw new ArgumentNullException("wanApiAddress");
  161. }
  162. var url = "Servers";
  163. url = GetConnectUrl(url);
  164. var postData = new Dictionary<string, string>
  165. {
  166. {"name", _appHost.FriendlyName},
  167. {"url", wanApiAddress},
  168. {"systemId", _appHost.SystemId}
  169. };
  170. if (!string.IsNullOrWhiteSpace(localAddress))
  171. {
  172. postData["localAddress"] = localAddress;
  173. }
  174. using (var stream = await _httpClient.Post(url, postData, CancellationToken.None).ConfigureAwait(false))
  175. {
  176. var data = _json.DeserializeFromStream<ServerRegistrationResponse>(stream);
  177. _data.ServerId = data.Id;
  178. _data.AccessKey = data.AccessKey;
  179. CacheData();
  180. }
  181. }
  182. private async Task UpdateServerRegistration(string wanApiAddress, string localAddress)
  183. {
  184. if (string.IsNullOrWhiteSpace(wanApiAddress))
  185. {
  186. throw new ArgumentNullException("wanApiAddress");
  187. }
  188. if (string.IsNullOrWhiteSpace(ConnectServerId))
  189. {
  190. throw new ArgumentNullException("ConnectServerId");
  191. }
  192. var url = "Servers";
  193. url = GetConnectUrl(url);
  194. url += "?id=" + ConnectServerId;
  195. var postData = new Dictionary<string, string>
  196. {
  197. {"name", _appHost.FriendlyName},
  198. {"url", wanApiAddress},
  199. {"systemId", _appHost.SystemId}
  200. };
  201. if (!string.IsNullOrWhiteSpace(localAddress))
  202. {
  203. postData["localAddress"] = localAddress;
  204. }
  205. var options = new HttpRequestOptions
  206. {
  207. Url = url,
  208. CancellationToken = CancellationToken.None
  209. };
  210. options.SetPostData(postData);
  211. SetServerAccessToken(options);
  212. // No need to examine the response
  213. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  214. {
  215. }
  216. }
  217. private readonly object _dataFileLock = new object();
  218. private string CacheFilePath
  219. {
  220. get { return Path.Combine(_appPaths.DataPath, "connect.txt"); }
  221. }
  222. private void CacheData()
  223. {
  224. var path = CacheFilePath;
  225. try
  226. {
  227. Directory.CreateDirectory(Path.GetDirectoryName(path));
  228. var json = _json.SerializeToString(_data);
  229. var encrypted = _encryption.EncryptString(json);
  230. lock (_dataFileLock)
  231. {
  232. File.WriteAllText(path, encrypted, Encoding.UTF8);
  233. }
  234. }
  235. catch (Exception ex)
  236. {
  237. _logger.ErrorException("Error saving data", ex);
  238. }
  239. }
  240. private void LoadCachedData()
  241. {
  242. var path = CacheFilePath;
  243. try
  244. {
  245. lock (_dataFileLock)
  246. {
  247. var encrypted = File.ReadAllText(path, Encoding.UTF8);
  248. var json = _encryption.DecryptString(encrypted);
  249. _data = _json.DeserializeFromString<ConnectData>(json);
  250. }
  251. }
  252. catch (IOException)
  253. {
  254. // File isn't there. no biggie
  255. }
  256. catch (Exception ex)
  257. {
  258. _logger.ErrorException("Error loading data", ex);
  259. }
  260. }
  261. private User GetUser(string id)
  262. {
  263. var user = _userManager.GetUserById(id);
  264. if (user == null)
  265. {
  266. throw new ArgumentException("User not found.");
  267. }
  268. return user;
  269. }
  270. private string GetConnectUrl(string handler)
  271. {
  272. return "https://connect.mediabrowser.tv/service/" + handler;
  273. }
  274. public async Task<UserLinkResult> LinkUser(string userId, string connectUsername)
  275. {
  276. await _operationLock.WaitAsync().ConfigureAwait(false);
  277. try
  278. {
  279. return await LinkUserInternal(userId, connectUsername).ConfigureAwait(false);
  280. }
  281. finally
  282. {
  283. _operationLock.Release();
  284. }
  285. }
  286. private async Task<UserLinkResult> LinkUserInternal(string userId, string connectUsername)
  287. {
  288. if (string.IsNullOrWhiteSpace(userId))
  289. {
  290. throw new ArgumentNullException("userId");
  291. }
  292. if (string.IsNullOrWhiteSpace(connectUsername))
  293. {
  294. throw new ArgumentNullException("connectUsername");
  295. }
  296. if (string.IsNullOrWhiteSpace(ConnectServerId))
  297. {
  298. throw new ArgumentNullException("ConnectServerId");
  299. }
  300. var connectUser = await GetConnectUser(new ConnectUserQuery
  301. {
  302. NameOrEmail = connectUsername
  303. }, CancellationToken.None).ConfigureAwait(false);
  304. if (!connectUser.IsActive)
  305. {
  306. throw new ArgumentException("The Media Browser account has been disabled.");
  307. }
  308. var user = GetUser(userId);
  309. if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
  310. {
  311. await RemoveConnect(user, connectUser.Id).ConfigureAwait(false);
  312. }
  313. var url = GetConnectUrl("ServerAuthorizations");
  314. var options = new HttpRequestOptions
  315. {
  316. Url = url,
  317. CancellationToken = CancellationToken.None
  318. };
  319. var accessToken = Guid.NewGuid().ToString("N");
  320. var postData = new Dictionary<string, string>
  321. {
  322. {"serverId", ConnectServerId},
  323. {"userId", connectUser.Id},
  324. {"userType", "Linked"},
  325. {"accessToken", accessToken}
  326. };
  327. options.SetPostData(postData);
  328. SetServerAccessToken(options);
  329. var result = new UserLinkResult();
  330. // No need to examine the response
  331. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  332. {
  333. var response = _json.DeserializeFromStream<ServerUserAuthorizationResponse>(stream);
  334. result.IsPending = string.Equals(response.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase);
  335. }
  336. user.ConnectAccessKey = accessToken;
  337. user.ConnectUserName = connectUser.Name;
  338. user.ConnectUserId = connectUser.Id;
  339. user.ConnectLinkType = UserLinkType.LinkedUser;
  340. await user.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  341. user.Configuration.SyncConnectImage = false;
  342. user.Configuration.SyncConnectName = false;
  343. _userManager.UpdateConfiguration(user, user.Configuration);
  344. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  345. return result;
  346. }
  347. public async Task<UserLinkResult> InviteUser(ConnectAuthorizationRequest request)
  348. {
  349. await _operationLock.WaitAsync().ConfigureAwait(false);
  350. try
  351. {
  352. return await InviteUserInternal(request).ConfigureAwait(false);
  353. }
  354. finally
  355. {
  356. _operationLock.Release();
  357. }
  358. }
  359. private async Task<UserLinkResult> InviteUserInternal(ConnectAuthorizationRequest request)
  360. {
  361. var connectUsername = request.ConnectUserName;
  362. var sendingUserId = request.SendingUserId;
  363. if (string.IsNullOrWhiteSpace(connectUsername))
  364. {
  365. throw new ArgumentNullException("connectUsername");
  366. }
  367. if (string.IsNullOrWhiteSpace(ConnectServerId))
  368. {
  369. throw new ArgumentNullException("ConnectServerId");
  370. }
  371. string connectUserId = null;
  372. var result = new UserLinkResult();
  373. try
  374. {
  375. var connectUser = await GetConnectUser(new ConnectUserQuery
  376. {
  377. NameOrEmail = connectUsername
  378. }, CancellationToken.None).ConfigureAwait(false);
  379. if (!connectUser.IsActive)
  380. {
  381. throw new ArgumentException("The Media Browser account has been disabled.");
  382. }
  383. connectUserId = connectUser.Id;
  384. result.GuestDisplayName = connectUser.Name;
  385. }
  386. catch (HttpException ex)
  387. {
  388. if (!ex.StatusCode.HasValue ||
  389. ex.StatusCode.Value != HttpStatusCode.NotFound ||
  390. !Validator.EmailIsValid(connectUsername))
  391. {
  392. throw;
  393. }
  394. }
  395. var sendingUser = GetUser(sendingUserId);
  396. var requesterUserName = sendingUser.ConnectUserName;
  397. if (string.IsNullOrWhiteSpace(requesterUserName))
  398. {
  399. requesterUserName = sendingUser.Name;
  400. }
  401. if (string.IsNullOrWhiteSpace(connectUserId))
  402. {
  403. return await SendNewUserInvitation(requesterUserName, connectUsername).ConfigureAwait(false);
  404. }
  405. var url = GetConnectUrl("ServerAuthorizations");
  406. var options = new HttpRequestOptions
  407. {
  408. Url = url,
  409. CancellationToken = CancellationToken.None
  410. };
  411. var accessToken = Guid.NewGuid().ToString("N");
  412. var postData = new Dictionary<string, string>
  413. {
  414. {"serverId", ConnectServerId},
  415. {"userId", connectUserId},
  416. {"userType", "Guest"},
  417. {"accessToken", accessToken},
  418. {"requesterUserName", requesterUserName}
  419. };
  420. options.SetPostData(postData);
  421. SetServerAccessToken(options);
  422. // No need to examine the response
  423. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  424. {
  425. var response = _json.DeserializeFromStream<ServerUserAuthorizationResponse>(stream);
  426. result.IsPending = string.Equals(response.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase);
  427. _data.PendingAuthorizations.Add(new ConnectAuthorizationInternal
  428. {
  429. ConnectUserId = response.UserId,
  430. Id = response.Id,
  431. ImageUrl = response.UserImageUrl,
  432. UserName = response.UserName,
  433. ExcludedLibraries = request.ExcludedLibraries,
  434. ExcludedChannels = request.ExcludedChannels,
  435. EnableLiveTv = request.EnableLiveTv,
  436. AccessToken = accessToken
  437. });
  438. CacheData();
  439. }
  440. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  441. return result;
  442. }
  443. private async Task<UserLinkResult> SendNewUserInvitation(string fromName, string email)
  444. {
  445. var url = GetConnectUrl("users/invite");
  446. var options = new HttpRequestOptions
  447. {
  448. Url = url,
  449. CancellationToken = CancellationToken.None
  450. };
  451. var postData = new Dictionary<string, string>
  452. {
  453. {"email", email},
  454. {"requesterUserName", fromName}
  455. };
  456. options.SetPostData(postData);
  457. // No need to examine the response
  458. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  459. {
  460. }
  461. return new UserLinkResult
  462. {
  463. IsNewUserInvitation = true,
  464. GuestDisplayName = email
  465. };
  466. }
  467. public Task RemoveConnect(string userId)
  468. {
  469. var user = GetUser(userId);
  470. return RemoveConnect(user, user.ConnectUserId);
  471. }
  472. private async Task RemoveConnect(User user, string connectUserId)
  473. {
  474. if (!string.IsNullOrWhiteSpace(connectUserId))
  475. {
  476. await CancelAuthorizationByConnectUserId(connectUserId).ConfigureAwait(false);
  477. }
  478. user.ConnectAccessKey = null;
  479. user.ConnectUserName = null;
  480. user.ConnectUserId = null;
  481. user.ConnectLinkType = null;
  482. await user.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  483. }
  484. private async Task<ConnectUser> GetConnectUser(ConnectUserQuery query, CancellationToken cancellationToken)
  485. {
  486. var url = GetConnectUrl("user");
  487. if (!string.IsNullOrWhiteSpace(query.Id))
  488. {
  489. url = url + "?id=" + WebUtility.UrlEncode(query.Id);
  490. }
  491. else if (!string.IsNullOrWhiteSpace(query.NameOrEmail))
  492. {
  493. url = url + "?nameOrEmail=" + WebUtility.UrlEncode(query.NameOrEmail);
  494. }
  495. else if (!string.IsNullOrWhiteSpace(query.Name))
  496. {
  497. url = url + "?name=" + WebUtility.UrlEncode(query.Name);
  498. }
  499. else if (!string.IsNullOrWhiteSpace(query.Email))
  500. {
  501. url = url + "?name=" + WebUtility.UrlEncode(query.Email);
  502. }
  503. else
  504. {
  505. throw new ArgumentException("Empty ConnectUserQuery supplied");
  506. }
  507. var options = new HttpRequestOptions
  508. {
  509. CancellationToken = cancellationToken,
  510. Url = url
  511. };
  512. SetServerAccessToken(options);
  513. using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
  514. {
  515. var response = _json.DeserializeFromStream<GetConnectUserResponse>(stream);
  516. return new ConnectUser
  517. {
  518. Email = response.Email,
  519. Id = response.Id,
  520. Name = response.Name,
  521. IsActive = response.IsActive,
  522. ImageUrl = response.ImageUrl
  523. };
  524. }
  525. }
  526. private void SetServerAccessToken(HttpRequestOptions options)
  527. {
  528. if (string.IsNullOrWhiteSpace(ConnectAccessKey))
  529. {
  530. throw new ArgumentNullException("ConnectAccessKey");
  531. }
  532. options.RequestHeaders.Add("X-Connect-Token", ConnectAccessKey);
  533. }
  534. public async Task RefreshAuthorizations(CancellationToken cancellationToken)
  535. {
  536. await _operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  537. try
  538. {
  539. await RefreshAuthorizationsInternal(true, cancellationToken).ConfigureAwait(false);
  540. }
  541. finally
  542. {
  543. _operationLock.Release();
  544. }
  545. }
  546. private async Task RefreshAuthorizationsInternal(bool refreshImages, CancellationToken cancellationToken)
  547. {
  548. if (string.IsNullOrWhiteSpace(ConnectServerId))
  549. {
  550. throw new ArgumentNullException("ConnectServerId");
  551. }
  552. var url = GetConnectUrl("ServerAuthorizations");
  553. url += "?serverId=" + ConnectServerId;
  554. var options = new HttpRequestOptions
  555. {
  556. Url = url,
  557. CancellationToken = cancellationToken
  558. };
  559. SetServerAccessToken(options);
  560. try
  561. {
  562. using (var stream = (await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)).Content)
  563. {
  564. var list = _json.DeserializeFromStream<List<ServerUserAuthorizationResponse>>(stream);
  565. await RefreshAuthorizations(list, refreshImages).ConfigureAwait(false);
  566. }
  567. }
  568. catch (Exception ex)
  569. {
  570. _logger.ErrorException("Error refreshing server authorizations.", ex);
  571. }
  572. }
  573. private readonly SemaphoreSlim _connectImageSemaphore = new SemaphoreSlim(5, 5);
  574. private async Task RefreshAuthorizations(List<ServerUserAuthorizationResponse> list, bool refreshImages)
  575. {
  576. var users = _userManager.Users.ToList();
  577. // Handle existing authorizations that were removed by the Connect server
  578. // Handle existing authorizations whose status may have been updated
  579. foreach (var user in users)
  580. {
  581. if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
  582. {
  583. var connectEntry = list.FirstOrDefault(i => string.Equals(i.UserId, user.ConnectUserId, StringComparison.OrdinalIgnoreCase));
  584. if (connectEntry == null)
  585. {
  586. var deleteUser = user.ConnectLinkType.HasValue &&
  587. user.ConnectLinkType.Value == UserLinkType.Guest;
  588. user.ConnectUserId = null;
  589. user.ConnectAccessKey = null;
  590. user.ConnectUserName = null;
  591. user.ConnectLinkType = null;
  592. await _userManager.UpdateUser(user).ConfigureAwait(false);
  593. if (deleteUser)
  594. {
  595. _logger.Debug("Deleting guest user {0}", user.Name);
  596. await _userManager.DeleteUser(user).ConfigureAwait(false);
  597. }
  598. }
  599. else
  600. {
  601. var changed = !string.Equals(user.ConnectAccessKey, connectEntry.AccessToken, StringComparison.OrdinalIgnoreCase);
  602. if (changed)
  603. {
  604. user.ConnectUserId = connectEntry.UserId;
  605. user.ConnectAccessKey = connectEntry.AccessToken;
  606. await _userManager.UpdateUser(user).ConfigureAwait(false);
  607. }
  608. }
  609. }
  610. }
  611. var currentPendingList = _data.PendingAuthorizations.ToList();
  612. var newPendingList = new List<ConnectAuthorizationInternal>();
  613. foreach (var connectEntry in list)
  614. {
  615. if (string.Equals(connectEntry.UserType, "guest", StringComparison.OrdinalIgnoreCase))
  616. {
  617. var currentPendingEntry = currentPendingList.FirstOrDefault(i => string.Equals(i.Id, connectEntry.Id, StringComparison.OrdinalIgnoreCase));
  618. if (string.Equals(connectEntry.AcceptStatus, "accepted", StringComparison.OrdinalIgnoreCase))
  619. {
  620. var user = _userManager.Users
  621. .FirstOrDefault(i => string.Equals(i.ConnectUserId, connectEntry.UserId, StringComparison.OrdinalIgnoreCase));
  622. if (user == null)
  623. {
  624. // Add user
  625. user = await _userManager.CreateUser(connectEntry.UserName).ConfigureAwait(false);
  626. user.ConnectUserName = connectEntry.UserName;
  627. user.ConnectUserId = connectEntry.UserId;
  628. user.ConnectLinkType = UserLinkType.Guest;
  629. user.ConnectAccessKey = connectEntry.AccessToken;
  630. await _userManager.UpdateUser(user).ConfigureAwait(false);
  631. user.Configuration.SyncConnectImage = true;
  632. user.Configuration.SyncConnectName = true;
  633. user.Configuration.IsHidden = true;
  634. user.Configuration.EnableLiveTvManagement = false;
  635. user.Configuration.EnableContentDeletion = false;
  636. user.Configuration.EnableRemoteControlOfOtherUsers = false;
  637. user.Configuration.IsAdministrator = false;
  638. if (currentPendingEntry != null)
  639. {
  640. user.Configuration.EnableLiveTvAccess = currentPendingEntry.EnableLiveTv;
  641. user.Configuration.BlockedMediaFolders = currentPendingEntry.ExcludedLibraries;
  642. user.Configuration.BlockedChannels = currentPendingEntry.ExcludedChannels;
  643. }
  644. _userManager.UpdateConfiguration(user, user.Configuration);
  645. }
  646. }
  647. else if (string.Equals(connectEntry.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase))
  648. {
  649. currentPendingEntry = currentPendingEntry ?? new ConnectAuthorizationInternal();
  650. currentPendingEntry.ConnectUserId = connectEntry.UserId;
  651. currentPendingEntry.ImageUrl = connectEntry.UserImageUrl;
  652. currentPendingEntry.UserName = connectEntry.UserName;
  653. currentPendingEntry.Id = connectEntry.Id;
  654. currentPendingEntry.AccessToken = connectEntry.AccessToken;
  655. newPendingList.Add(currentPendingEntry);
  656. }
  657. }
  658. }
  659. _data.PendingAuthorizations = newPendingList;
  660. CacheData();
  661. await RefreshGuestNames(list, refreshImages).ConfigureAwait(false);
  662. }
  663. private async Task RefreshGuestNames(List<ServerUserAuthorizationResponse> list, bool refreshImages)
  664. {
  665. var users = _userManager.Users
  666. .Where(i => !string.IsNullOrEmpty(i.ConnectUserId) &&
  667. (i.Configuration.SyncConnectImage || i.Configuration.SyncConnectName))
  668. .ToList();
  669. foreach (var user in users)
  670. {
  671. var authorization = list.FirstOrDefault(i => string.Equals(i.UserId, user.ConnectUserId, StringComparison.Ordinal));
  672. if (authorization == null)
  673. {
  674. _logger.Warn("Unable to find connect authorization record for user {0}", user.Name);
  675. continue;
  676. }
  677. if (user.Configuration.SyncConnectName)
  678. {
  679. var changed = !string.Equals(authorization.UserName, user.Name, StringComparison.OrdinalIgnoreCase);
  680. if (changed)
  681. {
  682. await user.Rename(authorization.UserName).ConfigureAwait(false);
  683. }
  684. }
  685. if (user.Configuration.SyncConnectImage)
  686. {
  687. var imageUrl = authorization.UserImageUrl;
  688. if (!string.IsNullOrWhiteSpace(imageUrl))
  689. {
  690. var changed = false;
  691. if (!user.HasImage(ImageType.Primary))
  692. {
  693. changed = true;
  694. }
  695. else if (refreshImages)
  696. {
  697. using (var response = await _httpClient.SendAsync(new HttpRequestOptions
  698. {
  699. Url = imageUrl,
  700. BufferContent = false
  701. }, "HEAD").ConfigureAwait(false))
  702. {
  703. var length = response.ContentLength;
  704. if (length != new FileInfo(user.GetImageInfo(ImageType.Primary, 0).Path).Length)
  705. {
  706. changed = true;
  707. }
  708. }
  709. }
  710. if (changed)
  711. {
  712. await _providerManager.SaveImage(user, imageUrl, _connectImageSemaphore, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false);
  713. await user.RefreshMetadata(new MetadataRefreshOptions
  714. {
  715. ForceSave = true,
  716. }, CancellationToken.None).ConfigureAwait(false);
  717. }
  718. }
  719. }
  720. }
  721. }
  722. public async Task<List<ConnectAuthorization>> GetPendingGuests()
  723. {
  724. var time = DateTime.UtcNow - _data.LastAuthorizationsRefresh;
  725. if (time.TotalMinutes >= 5)
  726. {
  727. await _operationLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
  728. try
  729. {
  730. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  731. _data.LastAuthorizationsRefresh = DateTime.UtcNow;
  732. CacheData();
  733. }
  734. finally
  735. {
  736. _operationLock.Release();
  737. }
  738. }
  739. return _data.PendingAuthorizations.Select(i => new ConnectAuthorization
  740. {
  741. ConnectUserId = i.ConnectUserId,
  742. EnableLiveTv = i.EnableLiveTv,
  743. ExcludedChannels = i.ExcludedChannels,
  744. ExcludedLibraries = i.ExcludedLibraries,
  745. Id = i.Id,
  746. ImageUrl = i.ImageUrl,
  747. UserName = i.UserName
  748. }).ToList();
  749. }
  750. public async Task CancelAuthorization(string id)
  751. {
  752. await _operationLock.WaitAsync().ConfigureAwait(false);
  753. try
  754. {
  755. await CancelAuthorizationInternal(id).ConfigureAwait(false);
  756. }
  757. finally
  758. {
  759. _operationLock.Release();
  760. }
  761. }
  762. private async Task CancelAuthorizationInternal(string id)
  763. {
  764. var connectUserId = _data.PendingAuthorizations
  765. .First(i => string.Equals(i.Id, id, StringComparison.Ordinal))
  766. .ConnectUserId;
  767. await CancelAuthorizationByConnectUserId(connectUserId).ConfigureAwait(false);
  768. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  769. }
  770. private async Task CancelAuthorizationByConnectUserId(string connectUserId)
  771. {
  772. if (string.IsNullOrWhiteSpace(connectUserId))
  773. {
  774. throw new ArgumentNullException("connectUserId");
  775. }
  776. if (string.IsNullOrWhiteSpace(ConnectServerId))
  777. {
  778. throw new ArgumentNullException("ConnectServerId");
  779. }
  780. var url = GetConnectUrl("ServerAuthorizations");
  781. var options = new HttpRequestOptions
  782. {
  783. Url = url,
  784. CancellationToken = CancellationToken.None
  785. };
  786. var postData = new Dictionary<string, string>
  787. {
  788. {"serverId", ConnectServerId},
  789. {"userId", connectUserId}
  790. };
  791. options.SetPostData(postData);
  792. SetServerAccessToken(options);
  793. try
  794. {
  795. // No need to examine the response
  796. using (var stream = (await _httpClient.SendAsync(options, "DELETE").ConfigureAwait(false)).Content)
  797. {
  798. }
  799. }
  800. catch (HttpException ex)
  801. {
  802. // If connect says the auth doesn't exist, we can handle that gracefully since this is a remove operation
  803. if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
  804. {
  805. throw;
  806. }
  807. _logger.Debug("Connect returned a 404 when removing a user auth link. Handling it.");
  808. }
  809. }
  810. public async Task Authenticate(string username, string passwordMd5)
  811. {
  812. if (string.IsNullOrWhiteSpace(username))
  813. {
  814. throw new ArgumentNullException("username");
  815. }
  816. if (string.IsNullOrWhiteSpace(passwordMd5))
  817. {
  818. throw new ArgumentNullException("passwordMd5");
  819. }
  820. var request = new HttpRequestOptions
  821. {
  822. Url = GetConnectUrl("user/authenticate")
  823. };
  824. request.SetPostData(new Dictionary<string, string>
  825. {
  826. {"userName",username},
  827. {"password",passwordMd5}
  828. });
  829. // No need to examine the response
  830. using (var stream = (await _httpClient.SendAsync(request, "POST").ConfigureAwait(false)).Content)
  831. {
  832. }
  833. }
  834. async void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e)
  835. {
  836. var user = e.Argument;
  837. await TryUploadUserPreferences(user, CancellationToken.None).ConfigureAwait(false);
  838. }
  839. private async Task TryUploadUserPreferences(User user, CancellationToken cancellationToken)
  840. {
  841. if (user == null)
  842. {
  843. throw new ArgumentNullException("user");
  844. }
  845. if (string.IsNullOrEmpty(user.ConnectUserId))
  846. {
  847. return;
  848. }
  849. if (string.IsNullOrEmpty(ConnectAccessKey))
  850. {
  851. return;
  852. }
  853. var url = GetConnectUrl("user/preferences");
  854. url += "?userId=" + user.ConnectUserId;
  855. url += "&key=userpreferences";
  856. var options = new HttpRequestOptions
  857. {
  858. Url = url,
  859. CancellationToken = cancellationToken
  860. };
  861. var postData = new Dictionary<string, string>();
  862. postData["data"] = _json.SerializeToString(ConnectUserPreferences.FromUserConfiguration(user.Configuration));
  863. options.SetPostData(postData);
  864. SetServerAccessToken(options);
  865. try
  866. {
  867. // No need to examine the response
  868. using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
  869. {
  870. }
  871. }
  872. catch (Exception ex)
  873. {
  874. _logger.ErrorException("Error uploading user preferences", ex);
  875. }
  876. }
  877. private async Task DownloadUserPreferences(User user, CancellationToken cancellationToken)
  878. {
  879. }
  880. public async Task<User> GetLocalUser(string connectUserId)
  881. {
  882. var user = _userManager.Users
  883. .FirstOrDefault(i => string.Equals(i.ConnectUserId, connectUserId, StringComparison.OrdinalIgnoreCase));
  884. if (user == null)
  885. {
  886. await RefreshAuthorizations(CancellationToken.None).ConfigureAwait(false);
  887. }
  888. return _userManager.Users
  889. .FirstOrDefault(i => string.Equals(i.ConnectUserId, connectUserId, StringComparison.OrdinalIgnoreCase));
  890. }
  891. public bool IsAuthorizationTokenValid(string token)
  892. {
  893. if (string.IsNullOrWhiteSpace(token))
  894. {
  895. throw new ArgumentNullException("token");
  896. }
  897. return _userManager.Users.Any(u => string.Equals(token, u.ConnectAccessKey, StringComparison.OrdinalIgnoreCase)) ||
  898. _data.PendingAuthorizations.Select(i => i.AccessToken).Contains(token, StringComparer.OrdinalIgnoreCase);
  899. }
  900. }
  901. }