ConnectManager.cs 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  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. var sendingUser = GetUser(sendingUserId);
  372. var requesterUserName = sendingUser.ConnectUserName;
  373. if (string.IsNullOrWhiteSpace(requesterUserName))
  374. {
  375. throw new ArgumentException("A Connect account is required in order to send invitations.");
  376. }
  377. string connectUserId = null;
  378. var result = new UserLinkResult();
  379. try
  380. {
  381. var connectUser = await GetConnectUser(new ConnectUserQuery
  382. {
  383. NameOrEmail = connectUsername
  384. }, CancellationToken.None).ConfigureAwait(false);
  385. if (!connectUser.IsActive)
  386. {
  387. throw new ArgumentException("The Media Browser account has been disabled.");
  388. }
  389. connectUserId = connectUser.Id;
  390. result.GuestDisplayName = connectUser.Name;
  391. }
  392. catch (HttpException ex)
  393. {
  394. if (!ex.StatusCode.HasValue ||
  395. ex.StatusCode.Value != HttpStatusCode.NotFound ||
  396. !Validator.EmailIsValid(connectUsername))
  397. {
  398. throw;
  399. }
  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.EnableSharedDeviceControl = false;
  638. user.Configuration.IsAdministrator = false;
  639. if (currentPendingEntry != null)
  640. {
  641. user.Configuration.EnableLiveTvAccess = currentPendingEntry.EnableLiveTv;
  642. user.Configuration.BlockedMediaFolders = currentPendingEntry.ExcludedLibraries;
  643. user.Configuration.BlockedChannels = currentPendingEntry.ExcludedChannels;
  644. }
  645. _userManager.UpdateConfiguration(user, user.Configuration);
  646. }
  647. }
  648. else if (string.Equals(connectEntry.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase))
  649. {
  650. currentPendingEntry = currentPendingEntry ?? new ConnectAuthorizationInternal();
  651. currentPendingEntry.ConnectUserId = connectEntry.UserId;
  652. currentPendingEntry.ImageUrl = connectEntry.UserImageUrl;
  653. currentPendingEntry.UserName = connectEntry.UserName;
  654. currentPendingEntry.Id = connectEntry.Id;
  655. currentPendingEntry.AccessToken = connectEntry.AccessToken;
  656. newPendingList.Add(currentPendingEntry);
  657. }
  658. }
  659. }
  660. _data.PendingAuthorizations = newPendingList;
  661. CacheData();
  662. await RefreshGuestNames(list, refreshImages).ConfigureAwait(false);
  663. }
  664. private async Task RefreshGuestNames(List<ServerUserAuthorizationResponse> list, bool refreshImages)
  665. {
  666. var users = _userManager.Users
  667. .Where(i => !string.IsNullOrEmpty(i.ConnectUserId) &&
  668. (i.Configuration.SyncConnectImage || i.Configuration.SyncConnectName))
  669. .ToList();
  670. foreach (var user in users)
  671. {
  672. var authorization = list.FirstOrDefault(i => string.Equals(i.UserId, user.ConnectUserId, StringComparison.Ordinal));
  673. if (authorization == null)
  674. {
  675. _logger.Warn("Unable to find connect authorization record for user {0}", user.Name);
  676. continue;
  677. }
  678. if (user.Configuration.SyncConnectName)
  679. {
  680. var changed = !string.Equals(authorization.UserName, user.Name, StringComparison.OrdinalIgnoreCase);
  681. if (changed)
  682. {
  683. await user.Rename(authorization.UserName).ConfigureAwait(false);
  684. }
  685. }
  686. if (user.Configuration.SyncConnectImage)
  687. {
  688. var imageUrl = authorization.UserImageUrl;
  689. if (!string.IsNullOrWhiteSpace(imageUrl))
  690. {
  691. var changed = false;
  692. if (!user.HasImage(ImageType.Primary))
  693. {
  694. changed = true;
  695. }
  696. else if (refreshImages)
  697. {
  698. using (var response = await _httpClient.SendAsync(new HttpRequestOptions
  699. {
  700. Url = imageUrl,
  701. BufferContent = false
  702. }, "HEAD").ConfigureAwait(false))
  703. {
  704. var length = response.ContentLength;
  705. if (length != new FileInfo(user.GetImageInfo(ImageType.Primary, 0).Path).Length)
  706. {
  707. changed = true;
  708. }
  709. }
  710. }
  711. if (changed)
  712. {
  713. await _providerManager.SaveImage(user, imageUrl, _connectImageSemaphore, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false);
  714. await user.RefreshMetadata(new MetadataRefreshOptions
  715. {
  716. ForceSave = true,
  717. }, CancellationToken.None).ConfigureAwait(false);
  718. }
  719. }
  720. }
  721. }
  722. }
  723. public async Task<List<ConnectAuthorization>> GetPendingGuests()
  724. {
  725. var time = DateTime.UtcNow - _data.LastAuthorizationsRefresh;
  726. if (time.TotalMinutes >= 5)
  727. {
  728. await _operationLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
  729. try
  730. {
  731. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  732. _data.LastAuthorizationsRefresh = DateTime.UtcNow;
  733. CacheData();
  734. }
  735. finally
  736. {
  737. _operationLock.Release();
  738. }
  739. }
  740. return _data.PendingAuthorizations.Select(i => new ConnectAuthorization
  741. {
  742. ConnectUserId = i.ConnectUserId,
  743. EnableLiveTv = i.EnableLiveTv,
  744. ExcludedChannels = i.ExcludedChannels,
  745. ExcludedLibraries = i.ExcludedLibraries,
  746. Id = i.Id,
  747. ImageUrl = i.ImageUrl,
  748. UserName = i.UserName
  749. }).ToList();
  750. }
  751. public async Task CancelAuthorization(string id)
  752. {
  753. await _operationLock.WaitAsync().ConfigureAwait(false);
  754. try
  755. {
  756. await CancelAuthorizationInternal(id).ConfigureAwait(false);
  757. }
  758. finally
  759. {
  760. _operationLock.Release();
  761. }
  762. }
  763. private async Task CancelAuthorizationInternal(string id)
  764. {
  765. var connectUserId = _data.PendingAuthorizations
  766. .First(i => string.Equals(i.Id, id, StringComparison.Ordinal))
  767. .ConnectUserId;
  768. await CancelAuthorizationByConnectUserId(connectUserId).ConfigureAwait(false);
  769. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  770. }
  771. private async Task CancelAuthorizationByConnectUserId(string connectUserId)
  772. {
  773. if (string.IsNullOrWhiteSpace(connectUserId))
  774. {
  775. throw new ArgumentNullException("connectUserId");
  776. }
  777. if (string.IsNullOrWhiteSpace(ConnectServerId))
  778. {
  779. throw new ArgumentNullException("ConnectServerId");
  780. }
  781. var url = GetConnectUrl("ServerAuthorizations");
  782. var options = new HttpRequestOptions
  783. {
  784. Url = url,
  785. CancellationToken = CancellationToken.None
  786. };
  787. var postData = new Dictionary<string, string>
  788. {
  789. {"serverId", ConnectServerId},
  790. {"userId", connectUserId}
  791. };
  792. options.SetPostData(postData);
  793. SetServerAccessToken(options);
  794. try
  795. {
  796. // No need to examine the response
  797. using (var stream = (await _httpClient.SendAsync(options, "DELETE").ConfigureAwait(false)).Content)
  798. {
  799. }
  800. }
  801. catch (HttpException ex)
  802. {
  803. // If connect says the auth doesn't exist, we can handle that gracefully since this is a remove operation
  804. if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
  805. {
  806. throw;
  807. }
  808. _logger.Debug("Connect returned a 404 when removing a user auth link. Handling it.");
  809. }
  810. }
  811. public async Task Authenticate(string username, string passwordMd5)
  812. {
  813. if (string.IsNullOrWhiteSpace(username))
  814. {
  815. throw new ArgumentNullException("username");
  816. }
  817. if (string.IsNullOrWhiteSpace(passwordMd5))
  818. {
  819. throw new ArgumentNullException("passwordMd5");
  820. }
  821. var request = new HttpRequestOptions
  822. {
  823. Url = GetConnectUrl("user/authenticate")
  824. };
  825. request.SetPostData(new Dictionary<string, string>
  826. {
  827. {"userName",username},
  828. {"password",passwordMd5}
  829. });
  830. // No need to examine the response
  831. using (var stream = (await _httpClient.SendAsync(request, "POST").ConfigureAwait(false)).Content)
  832. {
  833. }
  834. }
  835. async void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e)
  836. {
  837. var user = e.Argument;
  838. await TryUploadUserPreferences(user, CancellationToken.None).ConfigureAwait(false);
  839. }
  840. private async Task TryUploadUserPreferences(User user, CancellationToken cancellationToken)
  841. {
  842. if (user == null)
  843. {
  844. throw new ArgumentNullException("user");
  845. }
  846. if (string.IsNullOrEmpty(user.ConnectUserId))
  847. {
  848. return;
  849. }
  850. if (string.IsNullOrEmpty(ConnectAccessKey))
  851. {
  852. return;
  853. }
  854. var url = GetConnectUrl("user/preferences");
  855. url += "?userId=" + user.ConnectUserId;
  856. url += "&key=userpreferences";
  857. var options = new HttpRequestOptions
  858. {
  859. Url = url,
  860. CancellationToken = cancellationToken
  861. };
  862. var postData = new Dictionary<string, string>();
  863. postData["data"] = _json.SerializeToString(ConnectUserPreferences.FromUserConfiguration(user.Configuration));
  864. options.SetPostData(postData);
  865. SetServerAccessToken(options);
  866. try
  867. {
  868. // No need to examine the response
  869. using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
  870. {
  871. }
  872. }
  873. catch (Exception ex)
  874. {
  875. _logger.ErrorException("Error uploading user preferences", ex);
  876. }
  877. }
  878. private async Task DownloadUserPreferences(User user, CancellationToken cancellationToken)
  879. {
  880. }
  881. public async Task<User> GetLocalUser(string connectUserId)
  882. {
  883. var user = _userManager.Users
  884. .FirstOrDefault(i => string.Equals(i.ConnectUserId, connectUserId, StringComparison.OrdinalIgnoreCase));
  885. if (user == null)
  886. {
  887. await RefreshAuthorizations(CancellationToken.None).ConfigureAwait(false);
  888. }
  889. return _userManager.Users
  890. .FirstOrDefault(i => string.Equals(i.ConnectUserId, connectUserId, StringComparison.OrdinalIgnoreCase));
  891. }
  892. public bool IsAuthorizationTokenValid(string token)
  893. {
  894. if (string.IsNullOrWhiteSpace(token))
  895. {
  896. throw new ArgumentNullException("token");
  897. }
  898. return _userManager.Users.Any(u => string.Equals(token, u.ConnectAccessKey, StringComparison.OrdinalIgnoreCase)) ||
  899. _data.PendingAuthorizations.Select(i => i.AccessToken).Contains(token, StringComparer.OrdinalIgnoreCase);
  900. }
  901. }
  902. }