ConnectManager.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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.Security;
  9. using MediaBrowser.Model.Connect;
  10. using MediaBrowser.Model.Logging;
  11. using MediaBrowser.Model.Net;
  12. using MediaBrowser.Model.Serialization;
  13. using System;
  14. using System.Collections.Generic;
  15. using System.Globalization;
  16. using System.IO;
  17. using System.Linq;
  18. using System.Net;
  19. using System.Text;
  20. using System.Threading;
  21. using System.Threading.Tasks;
  22. namespace MediaBrowser.Server.Implementations.Connect
  23. {
  24. public class ConnectManager : IConnectManager
  25. {
  26. private SemaphoreSlim _operationLock = new SemaphoreSlim(1,1);
  27. private readonly ILogger _logger;
  28. private readonly IApplicationPaths _appPaths;
  29. private readonly IJsonSerializer _json;
  30. private readonly IEncryptionManager _encryption;
  31. private readonly IHttpClient _httpClient;
  32. private readonly IServerApplicationHost _appHost;
  33. private readonly IServerConfigurationManager _config;
  34. private readonly IUserManager _userManager;
  35. private ConnectData _data = new ConnectData();
  36. public string ConnectServerId
  37. {
  38. get { return _data.ServerId; }
  39. }
  40. public string ConnectAccessKey
  41. {
  42. get { return _data.AccessKey; }
  43. }
  44. public string DiscoveredWanIpAddress { get; private set; }
  45. public string WanIpAddress
  46. {
  47. get
  48. {
  49. var address = _config.Configuration.WanDdns;
  50. if (string.IsNullOrWhiteSpace(address))
  51. {
  52. address = DiscoveredWanIpAddress;
  53. }
  54. return address;
  55. }
  56. }
  57. public string WanApiAddress
  58. {
  59. get
  60. {
  61. var ip = WanIpAddress;
  62. if (!string.IsNullOrEmpty(ip))
  63. {
  64. if (!ip.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
  65. !ip.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
  66. {
  67. ip = "http://" + ip;
  68. }
  69. return ip + ":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture);
  70. }
  71. return null;
  72. }
  73. }
  74. public ConnectManager(ILogger logger,
  75. IApplicationPaths appPaths,
  76. IJsonSerializer json,
  77. IEncryptionManager encryption,
  78. IHttpClient httpClient,
  79. IServerApplicationHost appHost,
  80. IServerConfigurationManager config, IUserManager userManager)
  81. {
  82. _logger = logger;
  83. _appPaths = appPaths;
  84. _json = json;
  85. _encryption = encryption;
  86. _httpClient = httpClient;
  87. _appHost = appHost;
  88. _config = config;
  89. _userManager = userManager;
  90. LoadCachedData();
  91. }
  92. internal void OnWanAddressResolved(string address)
  93. {
  94. DiscoveredWanIpAddress = address;
  95. UpdateConnectInfo();
  96. }
  97. private async void UpdateConnectInfo()
  98. {
  99. await _operationLock.WaitAsync().ConfigureAwait(false);
  100. try
  101. {
  102. await UpdateConnectInfoInternal().ConfigureAwait(false);
  103. }
  104. finally
  105. {
  106. _operationLock.Release();
  107. }
  108. }
  109. private async Task UpdateConnectInfoInternal()
  110. {
  111. var wanApiAddress = WanApiAddress;
  112. if (string.IsNullOrWhiteSpace(wanApiAddress))
  113. {
  114. _logger.Warn("Cannot update Media Browser Connect information without a WanApiAddress");
  115. return;
  116. }
  117. try
  118. {
  119. var hasExistingRecord = !string.IsNullOrWhiteSpace(ConnectServerId) &&
  120. !string.IsNullOrWhiteSpace(ConnectAccessKey);
  121. var createNewRegistration = !hasExistingRecord;
  122. if (hasExistingRecord)
  123. {
  124. try
  125. {
  126. await UpdateServerRegistration(wanApiAddress).ConfigureAwait(false);
  127. }
  128. catch (HttpException ex)
  129. {
  130. if (!ex.StatusCode.HasValue || !new[] { HttpStatusCode.NotFound, HttpStatusCode.Unauthorized }.Contains(ex.StatusCode.Value))
  131. {
  132. throw;
  133. }
  134. createNewRegistration = true;
  135. }
  136. }
  137. if (createNewRegistration)
  138. {
  139. await CreateServerRegistration(wanApiAddress).ConfigureAwait(false);
  140. }
  141. await RefreshAuthorizationsInternal(CancellationToken.None).ConfigureAwait(false);
  142. }
  143. catch (Exception ex)
  144. {
  145. _logger.ErrorException("Error registering with Connect", ex);
  146. }
  147. }
  148. private async Task CreateServerRegistration(string wanApiAddress)
  149. {
  150. var url = "Servers";
  151. url = GetConnectUrl(url);
  152. var postData = new Dictionary<string, string>
  153. {
  154. {"name", _appHost.FriendlyName},
  155. {"url", wanApiAddress},
  156. {"systemid", _appHost.SystemId}
  157. };
  158. using (var stream = await _httpClient.Post(url, postData, CancellationToken.None).ConfigureAwait(false))
  159. {
  160. var data = _json.DeserializeFromStream<ServerRegistrationResponse>(stream);
  161. _data.ServerId = data.Id;
  162. _data.AccessKey = data.AccessKey;
  163. CacheData();
  164. }
  165. }
  166. private async Task UpdateServerRegistration(string wanApiAddress)
  167. {
  168. var url = "Servers";
  169. url = GetConnectUrl(url);
  170. url += "?id=" + ConnectServerId;
  171. var options = new HttpRequestOptions
  172. {
  173. Url = url,
  174. CancellationToken = CancellationToken.None
  175. };
  176. options.SetPostData(new Dictionary<string, string>
  177. {
  178. {"name", _appHost.FriendlyName},
  179. {"url", wanApiAddress},
  180. {"systemid", _appHost.SystemId}
  181. });
  182. SetServerAccessToken(options);
  183. // No need to examine the response
  184. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  185. {
  186. }
  187. }
  188. private string CacheFilePath
  189. {
  190. get { return Path.Combine(_appPaths.DataPath, "connect.txt"); }
  191. }
  192. private void CacheData()
  193. {
  194. var path = CacheFilePath;
  195. try
  196. {
  197. Directory.CreateDirectory(Path.GetDirectoryName(path));
  198. var json = _json.SerializeToString(_data);
  199. var encrypted = _encryption.EncryptString(json);
  200. File.WriteAllText(path, encrypted, Encoding.UTF8);
  201. }
  202. catch (Exception ex)
  203. {
  204. _logger.ErrorException("Error saving data", ex);
  205. }
  206. }
  207. private void LoadCachedData()
  208. {
  209. var path = CacheFilePath;
  210. try
  211. {
  212. var encrypted = File.ReadAllText(path, Encoding.UTF8);
  213. var json = _encryption.DecryptString(encrypted);
  214. _data = _json.DeserializeFromString<ConnectData>(json);
  215. }
  216. catch (IOException)
  217. {
  218. // File isn't there. no biggie
  219. }
  220. catch (Exception ex)
  221. {
  222. _logger.ErrorException("Error loading data", ex);
  223. }
  224. }
  225. private User GetUser(string id)
  226. {
  227. var user = _userManager.GetUserById(id);
  228. if (user == null)
  229. {
  230. throw new ArgumentException("User not found.");
  231. }
  232. return user;
  233. }
  234. private string GetConnectUrl(string handler)
  235. {
  236. return "https://connect.mediabrowser.tv/service/" + handler;
  237. }
  238. public async Task<UserLinkResult> LinkUser(string userId, string connectUsername)
  239. {
  240. if (string.IsNullOrWhiteSpace(connectUsername))
  241. {
  242. throw new ArgumentNullException("connectUsername");
  243. }
  244. var connectUser = await GetConnectUser(new ConnectUserQuery
  245. {
  246. Name = connectUsername
  247. }, CancellationToken.None).ConfigureAwait(false);
  248. if (!connectUser.IsActive)
  249. {
  250. throw new ArgumentException("The Media Browser account has been disabled.");
  251. }
  252. var user = GetUser(userId);
  253. if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
  254. {
  255. await RemoveLink(user, connectUser.Id).ConfigureAwait(false);
  256. }
  257. var url = GetConnectUrl("ServerAuthorizations");
  258. var options = new HttpRequestOptions
  259. {
  260. Url = url,
  261. CancellationToken = CancellationToken.None
  262. };
  263. var accessToken = Guid.NewGuid().ToString("N");
  264. var postData = new Dictionary<string, string>
  265. {
  266. {"serverId", ConnectServerId},
  267. {"userId", connectUser.Id},
  268. {"userType", "Linked"},
  269. {"accessToken", accessToken}
  270. };
  271. options.SetPostData(postData);
  272. SetServerAccessToken(options);
  273. var result = new UserLinkResult();
  274. // No need to examine the response
  275. using (var stream = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
  276. {
  277. var response = _json.DeserializeFromStream<ServerUserAuthorizationResponse>(stream);
  278. result.IsPending = string.Equals(response.AcceptStatus, "waiting", StringComparison.OrdinalIgnoreCase);
  279. }
  280. user.ConnectAccessKey = accessToken;
  281. user.ConnectUserName = connectUser.Name;
  282. user.ConnectUserId = connectUser.Id;
  283. user.ConnectLinkType = UserLinkType.LinkedUser;
  284. await user.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  285. return result;
  286. }
  287. public Task RemoveLink(string userId)
  288. {
  289. var user = GetUser(userId);
  290. return RemoveLink(user, user.ConnectUserId);
  291. }
  292. private async Task RemoveLink(User user, string connectUserId)
  293. {
  294. if (!string.IsNullOrWhiteSpace(connectUserId))
  295. {
  296. var url = GetConnectUrl("ServerAuthorizations");
  297. var options = new HttpRequestOptions
  298. {
  299. Url = url,
  300. CancellationToken = CancellationToken.None
  301. };
  302. var postData = new Dictionary<string, string>
  303. {
  304. {"serverId", ConnectServerId},
  305. {"userId", connectUserId}
  306. };
  307. options.SetPostData(postData);
  308. SetServerAccessToken(options);
  309. try
  310. {
  311. // No need to examine the response
  312. using (var stream = (await _httpClient.SendAsync(options, "DELETE").ConfigureAwait(false)).Content)
  313. {
  314. }
  315. }
  316. catch (HttpException ex)
  317. {
  318. // If connect says the auth doesn't exist, we can handle that gracefully since this is a remove operation
  319. if (!ex.StatusCode.HasValue || ex.StatusCode.Value != HttpStatusCode.NotFound)
  320. {
  321. throw;
  322. }
  323. _logger.Debug("Connect returned a 404 when removing a user auth link. Handling it.");
  324. }
  325. }
  326. user.ConnectAccessKey = null;
  327. user.ConnectUserName = null;
  328. user.ConnectUserId = null;
  329. user.ConnectLinkType = UserLinkType.LinkedUser;
  330. await user.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
  331. }
  332. private async Task<ConnectUser> GetConnectUser(ConnectUserQuery query, CancellationToken cancellationToken)
  333. {
  334. var url = GetConnectUrl("user");
  335. if (!string.IsNullOrWhiteSpace(query.Id))
  336. {
  337. url = url + "?id=" + WebUtility.UrlEncode(query.Id);
  338. }
  339. else if (!string.IsNullOrWhiteSpace(query.Name))
  340. {
  341. url = url + "?name=" + WebUtility.UrlEncode(query.Name);
  342. }
  343. else if (!string.IsNullOrWhiteSpace(query.Email))
  344. {
  345. url = url + "?name=" + WebUtility.UrlEncode(query.Email);
  346. }
  347. var options = new HttpRequestOptions
  348. {
  349. CancellationToken = cancellationToken,
  350. Url = url
  351. };
  352. SetServerAccessToken(options);
  353. using (var stream = await _httpClient.Get(options).ConfigureAwait(false))
  354. {
  355. var response = _json.DeserializeFromStream<GetConnectUserResponse>(stream);
  356. return new ConnectUser
  357. {
  358. Email = response.Email,
  359. Id = response.Id,
  360. Name = response.Name,
  361. IsActive = response.IsActive
  362. };
  363. }
  364. }
  365. private void SetServerAccessToken(HttpRequestOptions options)
  366. {
  367. options.RequestHeaders.Add("X-Connect-Token", ConnectAccessKey);
  368. }
  369. public async Task RefreshAuthorizations(CancellationToken cancellationToken)
  370. {
  371. await _operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
  372. try
  373. {
  374. await RefreshAuthorizationsInternal(cancellationToken).ConfigureAwait(false);
  375. }
  376. finally
  377. {
  378. _operationLock.Release();
  379. }
  380. }
  381. private async Task RefreshAuthorizationsInternal(CancellationToken cancellationToken)
  382. {
  383. var url = GetConnectUrl("ServerAuthorizations");
  384. var options = new HttpRequestOptions
  385. {
  386. Url = url,
  387. CancellationToken = cancellationToken
  388. };
  389. var postData = new Dictionary<string, string>
  390. {
  391. {"serverId", ConnectServerId}
  392. };
  393. options.SetPostData(postData);
  394. SetServerAccessToken(options);
  395. try
  396. {
  397. using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
  398. {
  399. var list = _json.DeserializeFromStream<List<ServerUserAuthorizationResponse>>(stream);
  400. await RefreshAuthorizations(list).ConfigureAwait(false);
  401. }
  402. }
  403. catch (Exception ex)
  404. {
  405. _logger.ErrorException("Error refreshing server authorizations.", ex);
  406. }
  407. }
  408. private async Task RefreshAuthorizations(List<ServerUserAuthorizationResponse> list)
  409. {
  410. var users = _userManager.Users.ToList();
  411. // Handle existing authorizations that were removed by the Connect server
  412. // Handle existing authorizations whose status may have been updated
  413. foreach (var user in users)
  414. {
  415. if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
  416. {
  417. var connectEntry = list.FirstOrDefault(i => string.Equals(i.UserId, user.ConnectUserId, StringComparison.OrdinalIgnoreCase));
  418. if (connectEntry == null)
  419. {
  420. user.ConnectUserId = null;
  421. user.ConnectAccessKey = null;
  422. user.ConnectUserName = null;
  423. await _userManager.UpdateUser(user).ConfigureAwait(false);
  424. if (user.ConnectLinkType == UserLinkType.Guest)
  425. {
  426. await _userManager.DeleteUser(user).ConfigureAwait(false);
  427. }
  428. }
  429. else
  430. {
  431. var changed = !string.Equals(user.ConnectAccessKey, connectEntry.AccessToken, StringComparison.OrdinalIgnoreCase);
  432. if (changed)
  433. {
  434. user.ConnectUserId = connectEntry.UserId;
  435. user.ConnectAccessKey = connectEntry.AccessToken;
  436. await _userManager.UpdateUser(user).ConfigureAwait(false);
  437. }
  438. }
  439. }
  440. }
  441. users = _userManager.Users.ToList();
  442. // TODO: Handle newly added guests that we don't know about
  443. foreach (var connectEntry in list)
  444. {
  445. if (string.Equals(connectEntry.UserType, "guest", StringComparison.OrdinalIgnoreCase) &&
  446. string.Equals(connectEntry.AcceptStatus, "accepted", StringComparison.OrdinalIgnoreCase))
  447. {
  448. var user = users.FirstOrDefault(i => string.Equals(i.ConnectUserId, connectEntry.UserId, StringComparison.OrdinalIgnoreCase));
  449. if (user == null)
  450. {
  451. // Add user
  452. }
  453. }
  454. }
  455. }
  456. }
  457. }