ConnectManager.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. using System.Security.Cryptography;
  2. using MediaBrowser.Common.Configuration;
  3. using MediaBrowser.Common.Extensions;
  4. using MediaBrowser.Common.Net;
  5. using MediaBrowser.Controller;
  6. using MediaBrowser.Controller.Configuration;
  7. using MediaBrowser.Controller.Connect;
  8. using MediaBrowser.Controller.Entities;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Controller.Providers;
  11. using MediaBrowser.Controller.Security;
  12. using MediaBrowser.Model.Connect;
  13. using MediaBrowser.Model.Entities;
  14. using MediaBrowser.Model.Logging;
  15. using MediaBrowser.Model.Net;
  16. using MediaBrowser.Model.Serialization;
  17. using System;
  18. using System.Collections.Generic;
  19. using System.Globalization;
  20. using System.IO;
  21. using System.Linq;
  22. using System.Net;
  23. using System.Text;
  24. using System.Threading;
  25. using System.Threading.Tasks;
  26. namespace MediaBrowser.Server.Implementations.Connect
  27. {
  28. public class ConnectManager : IConnectManager
  29. {
  30. private readonly SemaphoreSlim _operationLock = new SemaphoreSlim(1, 1);
  31. private readonly ILogger _logger;
  32. private readonly IApplicationPaths _appPaths;
  33. private readonly IJsonSerializer _json;
  34. private readonly IEncryptionManager _encryption;
  35. private readonly IHttpClient _httpClient;
  36. private readonly IServerApplicationHost _appHost;
  37. private readonly IServerConfigurationManager _config;
  38. private readonly IUserManager _userManager;
  39. private readonly IProviderManager _providerManager;
  40. private ConnectData _data = new ConnectData();
  41. public string ConnectServerId
  42. {
  43. get { return _data.ServerId; }
  44. }
  45. public string ConnectAccessKey
  46. {
  47. get { return _data.AccessKey; }
  48. }
  49. public string DiscoveredWanIpAddress { get; private set; }
  50. public string WanIpAddress
  51. {
  52. get
  53. {
  54. var address = _config.Configuration.WanDdns;
  55. if (string.IsNullOrWhiteSpace(address))
  56. {
  57. address = DiscoveredWanIpAddress;
  58. }
  59. return address;
  60. }
  61. }
  62. public string WanApiAddress
  63. {
  64. get
  65. {
  66. var ip = WanIpAddress;
  67. if (!string.IsNullOrEmpty(ip))
  68. {
  69. if (!ip.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
  70. !ip.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
  71. {
  72. ip = "http://" + ip;
  73. }
  74. return ip + ":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture);
  75. }
  76. return null;
  77. }
  78. }
  79. public ConnectManager(ILogger logger,
  80. IApplicationPaths appPaths,
  81. IJsonSerializer json,
  82. IEncryptionManager encryption,
  83. IHttpClient httpClient,
  84. IServerApplicationHost appHost,
  85. IServerConfigurationManager config, IUserManager userManager, IProviderManager providerManager)
  86. {
  87. _logger = logger;
  88. _appPaths = appPaths;
  89. _json = json;
  90. _encryption = encryption;
  91. _httpClient = httpClient;
  92. _appHost = appHost;
  93. _config = config;
  94. _userManager = userManager;
  95. _providerManager = providerManager;
  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 hasExistingRecord = !string.IsNullOrWhiteSpace(ConnectServerId) &&
  126. !string.IsNullOrWhiteSpace(ConnectAccessKey);
  127. var createNewRegistration = !hasExistingRecord;
  128. if (hasExistingRecord)
  129. {
  130. try
  131. {
  132. await UpdateServerRegistration(wanApiAddress).ConfigureAwait(false);
  133. }
  134. catch (HttpException ex)
  135. {
  136. if (!ex.StatusCode.HasValue || !new[] { HttpStatusCode.NotFound, HttpStatusCode.Unauthorized }.Contains(ex.StatusCode.Value))
  137. {
  138. throw;
  139. }
  140. createNewRegistration = true;
  141. }
  142. }
  143. if (createNewRegistration)
  144. {
  145. await CreateServerRegistration(wanApiAddress).ConfigureAwait(false);
  146. }
  147. await RefreshAuthorizationsInternal(true, CancellationToken.None).ConfigureAwait(false);
  148. }
  149. catch (Exception ex)
  150. {
  151. _logger.ErrorException("Error registering with Connect", ex);
  152. }
  153. }
  154. private async Task CreateServerRegistration(string wanApiAddress)
  155. {
  156. var url = "Servers";
  157. url = GetConnectUrl(url);
  158. var postData = new Dictionary<string, string>
  159. {
  160. {"name", _appHost.FriendlyName},
  161. {"url", wanApiAddress},
  162. {"systemId", _appHost.SystemId}
  163. };
  164. using (var stream = await _httpClient.Post(url, postData, CancellationToken.None).ConfigureAwait(false))
  165. {
  166. var data = _json.DeserializeFromStream<ServerRegistrationResponse>(stream);
  167. _data.ServerId = data.Id;
  168. _data.AccessKey = data.AccessKey;
  169. CacheData();
  170. }
  171. }
  172. private async Task UpdateServerRegistration(string wanApiAddress)
  173. {
  174. var url = "Servers";
  175. url = GetConnectUrl(url);
  176. url += "?id=" + ConnectServerId;
  177. var options = new HttpRequestOptions
  178. {
  179. Url = url,
  180. CancellationToken = CancellationToken.None
  181. };
  182. options.SetPostData(new Dictionary<string, string>
  183. {
  184. {"name", _appHost.FriendlyName},
  185. {"url", wanApiAddress},
  186. {"systemId", _appHost.SystemId}
  187. });
  188. SetServerAccessToken(options);
  189. // No need to examine the response
  190. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  191. {
  192. }
  193. }
  194. private readonly object _dataFileLock = new object();
  195. private string CacheFilePath
  196. {
  197. get { return Path.Combine(_appPaths.DataPath, "connect.txt"); }
  198. }
  199. private void CacheData()
  200. {
  201. var path = CacheFilePath;
  202. try
  203. {
  204. Directory.CreateDirectory(Path.GetDirectoryName(path));
  205. var json = _json.SerializeToString(_data);
  206. var encrypted = _encryption.EncryptString(json);
  207. lock (_dataFileLock)
  208. {
  209. File.WriteAllText(path, encrypted, Encoding.UTF8);
  210. }
  211. }
  212. catch (Exception ex)
  213. {
  214. _logger.ErrorException("Error saving data", ex);
  215. }
  216. }
  217. private void LoadCachedData()
  218. {
  219. var path = CacheFilePath;
  220. try
  221. {
  222. lock (_dataFileLock)
  223. {
  224. var encrypted = File.ReadAllText(path, Encoding.UTF8);
  225. var json = _encryption.DecryptString(encrypted);
  226. _data = _json.DeserializeFromString<ConnectData>(json);
  227. }
  228. }
  229. catch (IOException)
  230. {
  231. // File isn't there. no biggie
  232. }
  233. catch (Exception ex)
  234. {
  235. _logger.ErrorException("Error loading data", ex);
  236. }
  237. }
  238. private User GetUser(string id)
  239. {
  240. var user = _userManager.GetUserById(id);
  241. if (user == null)
  242. {
  243. throw new ArgumentException("User not found.");
  244. }
  245. return user;
  246. }
  247. private string GetConnectUrl(string handler)
  248. {
  249. return "https://connect.mediabrowser.tv/service/" + handler;
  250. }
  251. public async Task<UserLinkResult> LinkUser(string userId, string connectUsername)
  252. {
  253. await _operationLock.WaitAsync().ConfigureAwait(false);
  254. try
  255. {
  256. return await LinkUserInternal(userId, connectUsername).ConfigureAwait(false);
  257. }
  258. finally
  259. {
  260. _operationLock.Release();
  261. }
  262. }
  263. private async Task<UserLinkResult> LinkUserInternal(string userId, string connectUsername)
  264. {
  265. if (string.IsNullOrWhiteSpace(connectUsername))
  266. {
  267. throw new ArgumentNullException("connectUsername");
  268. }
  269. var connectUser = await GetConnectUser(new ConnectUserQuery
  270. {
  271. Name = connectUsername
  272. }, CancellationToken.None).ConfigureAwait(false);
  273. if (!connectUser.IsActive)
  274. {
  275. throw new ArgumentException("The Media Browser account has been disabled.");
  276. }
  277. var user = GetUser(userId);
  278. if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
  279. {
  280. await RemoveConnect(user, connectUser.Id).ConfigureAwait(false);
  281. }
  282. var url = GetConnectUrl("ServerAuthorizations");
  283. var options = new HttpRequestOptions
  284. {
  285. Url = url,
  286. CancellationToken = CancellationToken.None
  287. };
  288. var accessToken = Guid.NewGuid().ToString("N");
  289. var postData = new Dictionary<string, string>
  290. {
  291. {"serverId", ConnectServerId},
  292. {"userId", connectUser.Id},
  293. {"userType", "Linked"},
  294. {"accessToken", accessToken}
  295. };
  296. options.SetPostData(postData);
  297. SetServerAccessToken(options);
  298. var result = new UserLinkResult();
  299. // No need to examine the response
  300. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  301. {
  302. var response = _json.DeserializeFromStream<ServerUserAuthorizationResponse>(stream);
  303. result.IsPending = string.Equals(response.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase);
  304. }
  305. user.ConnectAccessKey = accessToken;
  306. user.ConnectUserName = connectUser.Name;
  307. user.ConnectUserId = connectUser.Id;
  308. user.ConnectLinkType = UserLinkType.LinkedUser;
  309. await user.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  310. user.Configuration.SyncConnectImage = false;
  311. user.Configuration.SyncConnectName = false;
  312. _userManager.UpdateConfiguration(user, user.Configuration);
  313. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  314. return result;
  315. }
  316. public async Task<UserLinkResult> InviteUser(string sendingUserId, string connectUsername)
  317. {
  318. await _operationLock.WaitAsync().ConfigureAwait(false);
  319. try
  320. {
  321. return await InviteUserInternal(sendingUserId, connectUsername).ConfigureAwait(false);
  322. }
  323. finally
  324. {
  325. _operationLock.Release();
  326. }
  327. }
  328. private async Task<UserLinkResult> InviteUserInternal(string sendingUserId, string connectUsername)
  329. {
  330. if (string.IsNullOrWhiteSpace(connectUsername))
  331. {
  332. throw new ArgumentNullException("connectUsername");
  333. }
  334. var connectUser = await GetConnectUser(new ConnectUserQuery
  335. {
  336. Name = connectUsername
  337. }, CancellationToken.None).ConfigureAwait(false);
  338. if (!connectUser.IsActive)
  339. {
  340. throw new ArgumentException("The Media Browser account has been disabled.");
  341. }
  342. var url = GetConnectUrl("ServerAuthorizations");
  343. var options = new HttpRequestOptions
  344. {
  345. Url = url,
  346. CancellationToken = CancellationToken.None
  347. };
  348. var accessToken = Guid.NewGuid().ToString("N");
  349. var sendingUser = GetUser(sendingUserId);
  350. var requesterUserName = sendingUser.ConnectUserName;
  351. if (string.IsNullOrWhiteSpace(requesterUserName))
  352. {
  353. requesterUserName = sendingUser.Name;
  354. }
  355. var postData = new Dictionary<string, string>
  356. {
  357. {"serverId", ConnectServerId},
  358. {"userId", connectUser.Id},
  359. {"userType", "Guest"},
  360. {"accessToken", accessToken},
  361. {"requesterUserName", requesterUserName}
  362. };
  363. options.SetPostData(postData);
  364. SetServerAccessToken(options);
  365. var result = new UserLinkResult();
  366. // No need to examine the response
  367. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  368. {
  369. var response = _json.DeserializeFromStream<ServerUserAuthorizationResponse>(stream);
  370. result.IsPending = string.Equals(response.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase);
  371. }
  372. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  373. return result;
  374. }
  375. public Task RemoveConnect(string userId)
  376. {
  377. var user = GetUser(userId);
  378. return RemoveConnect(user, user.ConnectUserId);
  379. }
  380. private async Task RemoveConnect(User user, string connectUserId)
  381. {
  382. if (!string.IsNullOrWhiteSpace(connectUserId))
  383. {
  384. await CancelAuthorizationByConnectUserId(connectUserId).ConfigureAwait(false);
  385. }
  386. user.ConnectAccessKey = null;
  387. user.ConnectUserName = null;
  388. user.ConnectUserId = null;
  389. user.ConnectLinkType = null;
  390. await user.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  391. }
  392. private async Task<ConnectUser> GetConnectUser(ConnectUserQuery query, CancellationToken cancellationToken)
  393. {
  394. var url = GetConnectUrl("user");
  395. if (!string.IsNullOrWhiteSpace(query.Id))
  396. {
  397. url = url + "?id=" + WebUtility.UrlEncode(query.Id);
  398. }
  399. else if (!string.IsNullOrWhiteSpace(query.Name))
  400. {
  401. url = url + "?name=" + WebUtility.UrlEncode(query.Name);
  402. }
  403. else if (!string.IsNullOrWhiteSpace(query.Email))
  404. {
  405. url = url + "?name=" + WebUtility.UrlEncode(query.Email);
  406. }
  407. var options = new HttpRequestOptions
  408. {
  409. CancellationToken = cancellationToken,
  410. Url = url
  411. };
  412. SetServerAccessToken(options);
  413. using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
  414. {
  415. var response = _json.DeserializeFromStream<GetConnectUserResponse>(stream);
  416. return new ConnectUser
  417. {
  418. Email = response.Email,
  419. Id = response.Id,
  420. Name = response.Name,
  421. IsActive = response.IsActive,
  422. ImageUrl = response.ImageUrl
  423. };
  424. }
  425. }
  426. private void SetServerAccessToken(HttpRequestOptions options)
  427. {
  428. options.RequestHeaders.Add("X-Connect-Token", ConnectAccessKey);
  429. }
  430. public async Task RefreshAuthorizations(CancellationToken cancellationToken)
  431. {
  432. await _operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  433. try
  434. {
  435. await RefreshAuthorizationsInternal(true, cancellationToken).ConfigureAwait(false);
  436. }
  437. finally
  438. {
  439. _operationLock.Release();
  440. }
  441. }
  442. private async Task RefreshAuthorizationsInternal(bool refreshImages, CancellationToken cancellationToken)
  443. {
  444. var url = GetConnectUrl("ServerAuthorizations");
  445. url += "?serverId=" + ConnectServerId;
  446. var options = new HttpRequestOptions
  447. {
  448. Url = url,
  449. CancellationToken = cancellationToken
  450. };
  451. SetServerAccessToken(options);
  452. try
  453. {
  454. using (var stream = (await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)).Content)
  455. {
  456. var list = _json.DeserializeFromStream<List<ServerUserAuthorizationResponse>>(stream);
  457. await RefreshAuthorizations(list, refreshImages).ConfigureAwait(false);
  458. }
  459. }
  460. catch (Exception ex)
  461. {
  462. _logger.ErrorException("Error refreshing server authorizations.", ex);
  463. }
  464. }
  465. private readonly SemaphoreSlim _connectImageSemaphore = new SemaphoreSlim(5, 5);
  466. private async Task RefreshAuthorizations(List<ServerUserAuthorizationResponse> list, bool refreshImages)
  467. {
  468. var users = _userManager.Users.ToList();
  469. // Handle existing authorizations that were removed by the Connect server
  470. // Handle existing authorizations whose status may have been updated
  471. foreach (var user in users)
  472. {
  473. if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
  474. {
  475. var connectEntry = list.FirstOrDefault(i => string.Equals(i.UserId, user.ConnectUserId, StringComparison.OrdinalIgnoreCase));
  476. if (connectEntry == null)
  477. {
  478. user.ConnectUserId = null;
  479. user.ConnectAccessKey = null;
  480. user.ConnectUserName = null;
  481. user.ConnectLinkType = null;
  482. await _userManager.UpdateUser(user).ConfigureAwait(false);
  483. if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest)
  484. {
  485. _logger.Debug("Deleting guest user {0}", user.Name);
  486. await _userManager.DeleteUser(user).ConfigureAwait(false);
  487. }
  488. }
  489. else
  490. {
  491. var changed = !string.Equals(user.ConnectAccessKey, connectEntry.AccessToken, StringComparison.OrdinalIgnoreCase);
  492. if (changed)
  493. {
  494. user.ConnectUserId = connectEntry.UserId;
  495. user.ConnectAccessKey = connectEntry.AccessToken;
  496. await _userManager.UpdateUser(user).ConfigureAwait(false);
  497. }
  498. }
  499. }
  500. }
  501. var pending = new List<ConnectAuthorization>();
  502. foreach (var connectEntry in list)
  503. {
  504. if (string.Equals(connectEntry.UserType, "guest", StringComparison.OrdinalIgnoreCase))
  505. {
  506. if (string.Equals(connectEntry.AcceptStatus, "accepted", StringComparison.OrdinalIgnoreCase))
  507. {
  508. var user = _userManager.Users
  509. .FirstOrDefault(i => string.Equals(i.ConnectUserId, connectEntry.UserId, StringComparison.OrdinalIgnoreCase));
  510. if (user == null)
  511. {
  512. // Add user
  513. user = await _userManager.CreateUser(connectEntry.UserName).ConfigureAwait(false);
  514. user.ConnectUserName = connectEntry.UserName;
  515. user.ConnectUserId = connectEntry.UserId;
  516. user.ConnectLinkType = UserLinkType.Guest;
  517. user.ConnectAccessKey = connectEntry.AccessToken;
  518. await _userManager.UpdateUser(user).ConfigureAwait(false);
  519. user.Configuration.SyncConnectImage = true;
  520. user.Configuration.SyncConnectName = true;
  521. user.Configuration.IsHidden = true;
  522. user.Configuration.EnableLiveTvManagement = false;
  523. user.Configuration.IsAdministrator = false;
  524. _userManager.UpdateConfiguration(user, user.Configuration);
  525. }
  526. }
  527. else if (string.Equals(connectEntry.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase))
  528. {
  529. pending.Add(new ConnectAuthorization
  530. {
  531. ConnectUserId = connectEntry.UserId,
  532. ImageUrl = connectEntry.UserImageUrl,
  533. UserName = connectEntry.UserName,
  534. Id = connectEntry.Id
  535. });
  536. }
  537. }
  538. }
  539. _data.PendingAuthorizations = pending;
  540. CacheData();
  541. await RefreshGuestNames(list, refreshImages).ConfigureAwait(false);
  542. }
  543. private async Task RefreshGuestNames(List<ServerUserAuthorizationResponse> list, bool refreshImages)
  544. {
  545. var users = _userManager.Users
  546. .Where(i => !string.IsNullOrEmpty(i.ConnectUserId) &&
  547. (i.Configuration.SyncConnectImage || i.Configuration.SyncConnectName))
  548. .ToList();
  549. foreach (var user in users)
  550. {
  551. var authorization = list.FirstOrDefault(i => string.Equals(i.UserId, user.ConnectUserId, StringComparison.Ordinal));
  552. if (authorization == null)
  553. {
  554. _logger.Warn("Unable to find connect authorization record for user {0}", user.Name);
  555. continue;
  556. }
  557. if (user.Configuration.SyncConnectName)
  558. {
  559. var changed = !string.Equals(authorization.UserName, user.Name, StringComparison.OrdinalIgnoreCase);
  560. if (changed)
  561. {
  562. await user.Rename(authorization.UserName).ConfigureAwait(false);
  563. }
  564. }
  565. if (user.Configuration.SyncConnectImage)
  566. {
  567. var imageUrl = authorization.UserImageUrl;
  568. if (!string.IsNullOrWhiteSpace(imageUrl))
  569. {
  570. var changed = false;
  571. if (!user.HasImage(ImageType.Primary))
  572. {
  573. changed = true;
  574. }
  575. else if (refreshImages)
  576. {
  577. using (var response = await _httpClient.SendAsync(new HttpRequestOptions
  578. {
  579. Url = imageUrl,
  580. BufferContent = false
  581. }, "HEAD").ConfigureAwait(false))
  582. {
  583. var length = response.ContentLength;
  584. if (length != new FileInfo(user.GetImageInfo(ImageType.Primary, 0).Path).Length)
  585. {
  586. changed = true;
  587. }
  588. }
  589. }
  590. if (changed)
  591. {
  592. await _providerManager.SaveImage(user, imageUrl, _connectImageSemaphore, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false);
  593. await user.RefreshMetadata(new MetadataRefreshOptions
  594. {
  595. ForceSave = true,
  596. }, CancellationToken.None).ConfigureAwait(false);
  597. }
  598. }
  599. }
  600. }
  601. }
  602. public async Task<List<ConnectAuthorization>> GetPendingGuests()
  603. {
  604. var time = DateTime.UtcNow - _data.LastAuthorizationsRefresh;
  605. if (time.TotalMinutes >= 5)
  606. {
  607. await _operationLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
  608. try
  609. {
  610. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  611. _data.LastAuthorizationsRefresh = DateTime.UtcNow;
  612. CacheData();
  613. }
  614. finally
  615. {
  616. _operationLock.Release();
  617. }
  618. }
  619. return _data.PendingAuthorizations.ToList();
  620. }
  621. public async Task CancelAuthorization(string id)
  622. {
  623. await _operationLock.WaitAsync().ConfigureAwait(false);
  624. try
  625. {
  626. await CancelAuthorizationInternal(id).ConfigureAwait(false);
  627. }
  628. finally
  629. {
  630. _operationLock.Release();
  631. }
  632. }
  633. private async Task CancelAuthorizationInternal(string id)
  634. {
  635. var connectUserId = _data.PendingAuthorizations
  636. .First(i => string.Equals(i.Id, id, StringComparison.Ordinal))
  637. .ConnectUserId;
  638. await CancelAuthorizationByConnectUserId(connectUserId).ConfigureAwait(false);
  639. await RefreshAuthorizationsInternal(false, CancellationToken.None).ConfigureAwait(false);
  640. }
  641. private async Task CancelAuthorizationByConnectUserId(string connectUserId)
  642. {
  643. var url = GetConnectUrl("ServerAuthorizations");
  644. var options = new HttpRequestOptions
  645. {
  646. Url = url,
  647. CancellationToken = CancellationToken.None
  648. };
  649. var postData = new Dictionary<string, string>
  650. {
  651. {"serverId", ConnectServerId},
  652. {"userId", connectUserId}
  653. };
  654. options.SetPostData(postData);
  655. SetServerAccessToken(options);
  656. try
  657. {
  658. // No need to examine the response
  659. using (var stream = (await _httpClient.SendAsync(options, "DELETE").ConfigureAwait(false)).Content)
  660. {
  661. }
  662. }
  663. catch (HttpException ex)
  664. {
  665. // If connect says the auth doesn't exist, we can handle that gracefully since this is a remove operation
  666. if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
  667. {
  668. throw;
  669. }
  670. _logger.Debug("Connect returned a 404 when removing a user auth link. Handling it.");
  671. }
  672. }
  673. public async Task Authenticate(string username, string passwordMd5)
  674. {
  675. var request = new HttpRequestOptions
  676. {
  677. Url = GetConnectUrl("user/authenticate")
  678. };
  679. request.SetPostData(new Dictionary<string, string>
  680. {
  681. {"userName",username},
  682. {"password",passwordMd5}
  683. });
  684. // No need to examine the response
  685. using (var stream = (await _httpClient.SendAsync(request, "POST").ConfigureAwait(false)).Content)
  686. {
  687. }
  688. }
  689. }
  690. }