aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Security
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2016-12-18 00:44:33 -0500
committerGitHub <noreply@github.com>2016-12-18 00:44:33 -0500
commite7cebb91a73354dc3e0d0b6340c9fbd6511f4406 (patch)
tree6f1c368c766c17b7514fe749c0e92e69cd89194a /Emby.Server.Implementations/Security
parent025905a3e4d50b9a2e07fbf4ff0a203af6604ced (diff)
parentaaa027f3229073e9a40756c3157d41af2a442922 (diff)
Merge pull request #2350 from MediaBrowser/beta
Beta
Diffstat (limited to 'Emby.Server.Implementations/Security')
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs318
-rw-r--r--Emby.Server.Implementations/Security/EncryptionManager.cs51
-rw-r--r--Emby.Server.Implementations/Security/MBLicenseFile.cs214
-rw-r--r--Emby.Server.Implementations/Security/PluginSecurityManager.cs355
-rw-r--r--Emby.Server.Implementations/Security/RegRecord.cs12
5 files changed, 950 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
new file mode 100644
index 000000000..a2d61873b
--- /dev/null
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -0,0 +1,318 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Server.Implementations.Data;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Querying;
+using SQLitePCL.pretty;
+
+namespace Emby.Server.Implementations.Security
+{
+ public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
+ {
+ private readonly IServerApplicationPaths _appPaths;
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ public AuthenticationRepository(ILogger logger, IServerApplicationPaths appPaths)
+ : base(logger)
+ {
+ _appPaths = appPaths;
+ DbFilePath = Path.Combine(appPaths.DataPath, "authentication.db");
+ }
+
+ public void Initialize()
+ {
+ using (var connection = CreateConnection())
+ {
+ RunDefaultInitialization(connection);
+
+ string[] queries = {
+
+ "create table if not exists AccessTokens (Id GUID PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT, AppName TEXT, AppVersion TEXT, DeviceName TEXT, UserId TEXT, IsActive BIT, DateCreated DATETIME NOT NULL, DateRevoked DATETIME)",
+ "create index if not exists idx_AccessTokens on AccessTokens(Id)"
+ };
+
+ connection.RunQueries(queries);
+
+ connection.RunInTransaction(db =>
+ {
+ var existingColumnNames = GetColumnNames(db, "AccessTokens");
+
+ AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames);
+
+ }, TransactionMode);
+ }
+ }
+
+ public Task Create(AuthenticationInfo info, CancellationToken cancellationToken)
+ {
+ info.Id = Guid.NewGuid().ToString("N");
+
+ return Update(info, cancellationToken);
+ }
+
+ public async Task Update(AuthenticationInfo info, CancellationToken cancellationToken)
+ {
+ if (info == null)
+ {
+ throw new ArgumentNullException("info");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using (WriteLock.Write())
+ {
+ using (var connection = CreateConnection())
+ {
+ connection.RunInTransaction(db =>
+ {
+ using (var statement = db.PrepareStatement("replace into AccessTokens (Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (@Id, @AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @IsActive, @DateCreated, @DateRevoked)"))
+ {
+ statement.TryBind("@Id", info.Id.ToGuidParamValue());
+ statement.TryBind("@AccessToken", info.AccessToken);
+
+ statement.TryBind("@DeviceId", info.DeviceId);
+ statement.TryBind("@AppName", info.AppName);
+ statement.TryBind("@AppVersion", info.AppVersion);
+ statement.TryBind("@DeviceName", info.DeviceName);
+ statement.TryBind("@UserId", info.UserId);
+ statement.TryBind("@IsActive", info.IsActive);
+ statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
+
+ if (info.DateRevoked.HasValue)
+ {
+ statement.TryBind("@DateRevoked", info.DateRevoked.Value.ToDateTimeParamValue());
+ }
+ else
+ {
+ statement.TryBindNull("@DateRevoked");
+ }
+
+ statement.MoveNext();
+ }
+
+ }, TransactionMode);
+ }
+ }
+ }
+
+ private const string BaseSelectText = "select Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked from AccessTokens";
+
+ private void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement)
+ {
+ if (!string.IsNullOrWhiteSpace(query.AccessToken))
+ {
+ statement.TryBind("@AccessToken", query.AccessToken);
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.UserId))
+ {
+ statement.TryBind("@UserId", query.UserId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.DeviceId))
+ {
+ statement.TryBind("@DeviceId", query.DeviceId);
+ }
+
+ if (query.IsActive.HasValue)
+ {
+ statement.TryBind("@IsActive", query.IsActive.Value);
+ }
+ }
+
+ public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query)
+ {
+ if (query == null)
+ {
+ throw new ArgumentNullException("query");
+ }
+
+ var commandText = BaseSelectText;
+
+ var whereClauses = new List<string>();
+
+ var startIndex = query.StartIndex ?? 0;
+
+ if (!string.IsNullOrWhiteSpace(query.AccessToken))
+ {
+ whereClauses.Add("AccessToken=@AccessToken");
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.UserId))
+ {
+ whereClauses.Add("UserId=@UserId");
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.DeviceId))
+ {
+ whereClauses.Add("DeviceId=@DeviceId");
+ }
+
+ if (query.IsActive.HasValue)
+ {
+ whereClauses.Add("IsActive=@IsActive");
+ }
+
+ if (query.HasUser.HasValue)
+ {
+ if (query.HasUser.Value)
+ {
+ whereClauses.Add("UserId not null");
+ }
+ else
+ {
+ whereClauses.Add("UserId is null");
+ }
+ }
+
+ var whereTextWithoutPaging = whereClauses.Count == 0 ?
+ string.Empty :
+ " where " + string.Join(" AND ", whereClauses.ToArray());
+
+ if (startIndex > 0)
+ {
+ var pagingWhereText = whereClauses.Count == 0 ?
+ string.Empty :
+ " where " + string.Join(" AND ", whereClauses.ToArray());
+
+ whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})",
+ pagingWhereText,
+ startIndex.ToString(_usCulture)));
+ }
+
+ var whereText = whereClauses.Count == 0 ?
+ string.Empty :
+ " where " + string.Join(" AND ", whereClauses.ToArray());
+
+ commandText += whereText;
+
+ commandText += " ORDER BY DateCreated";
+
+ if (query.Limit.HasValue)
+ {
+ commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture);
+ }
+
+ var list = new List<AuthenticationInfo>();
+
+ using (WriteLock.Read())
+ {
+ using (var connection = CreateConnection(true))
+ {
+ return connection.RunInTransaction(db =>
+ {
+ var result = new QueryResult<AuthenticationInfo>();
+
+ var statementTexts = new List<string>();
+ statementTexts.Add(commandText);
+ statementTexts.Add("select count (Id) from AccessTokens" + whereTextWithoutPaging);
+
+ var statements = PrepareAllSafe(db, statementTexts)
+ .ToList();
+
+ using (var statement = statements[0])
+ {
+ BindAuthenticationQueryParams(query, statement);
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ list.Add(Get(row));
+ }
+
+ using (var totalCountStatement = statements[1])
+ {
+ BindAuthenticationQueryParams(query, totalCountStatement);
+
+ result.TotalRecordCount = totalCountStatement.ExecuteQuery()
+ .SelectScalarInt()
+ .First();
+ }
+ }
+
+ result.Items = list.ToArray();
+ return result;
+
+ }, ReadTransactionMode);
+ }
+ }
+ }
+
+ public AuthenticationInfo Get(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new ArgumentNullException("id");
+ }
+
+ using (WriteLock.Read())
+ {
+ using (var connection = CreateConnection(true))
+ {
+ var commandText = BaseSelectText + " where Id=@Id";
+
+ using (var statement = connection.PrepareStatement(commandText))
+ {
+ statement.BindParameters["@Id"].Bind(id.ToGuidParamValue());
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ return Get(row);
+ }
+ return null;
+ }
+ }
+ }
+ }
+
+ private AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader)
+ {
+ var info = new AuthenticationInfo
+ {
+ Id = reader[0].ReadGuid().ToString("N"),
+ AccessToken = reader[1].ToString()
+ };
+
+ if (reader[2].SQLiteType != SQLiteType.Null)
+ {
+ info.DeviceId = reader[2].ToString();
+ }
+
+ if (reader[3].SQLiteType != SQLiteType.Null)
+ {
+ info.AppName = reader[3].ToString();
+ }
+
+ if (reader[4].SQLiteType != SQLiteType.Null)
+ {
+ info.AppVersion = reader[4].ToString();
+ }
+
+ if (reader[5].SQLiteType != SQLiteType.Null)
+ {
+ info.DeviceName = reader[5].ToString();
+ }
+
+ if (reader[6].SQLiteType != SQLiteType.Null)
+ {
+ info.UserId = reader[6].ToString();
+ }
+
+ info.IsActive = reader[7].ToBool();
+ info.DateCreated = reader[8].ReadDateTime();
+
+ if (reader[9].SQLiteType != SQLiteType.Null)
+ {
+ info.DateRevoked = reader[9].ReadDateTime();
+ }
+
+ return info;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Security/EncryptionManager.cs b/Emby.Server.Implementations/Security/EncryptionManager.cs
new file mode 100644
index 000000000..271b0bbdb
--- /dev/null
+++ b/Emby.Server.Implementations/Security/EncryptionManager.cs
@@ -0,0 +1,51 @@
+using MediaBrowser.Controller.Security;
+using System;
+using System.Text;
+
+namespace Emby.Server.Implementations.Security
+{
+ public class EncryptionManager : IEncryptionManager
+ {
+ /// <summary>
+ /// Encrypts the string.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">value</exception>
+ public string EncryptString(string value)
+ {
+ if (value == null) throw new ArgumentNullException("value");
+
+ return EncryptStringUniversal(value);
+ }
+
+ /// <summary>
+ /// Decrypts the string.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">value</exception>
+ public string DecryptString(string value)
+ {
+ if (value == null) throw new ArgumentNullException("value");
+
+ return DecryptStringUniversal(value);
+ }
+
+ private string EncryptStringUniversal(string value)
+ {
+ // Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now
+
+ var bytes = Encoding.UTF8.GetBytes(value);
+ return Convert.ToBase64String(bytes);
+ }
+
+ private string DecryptStringUniversal(string value)
+ {
+ // Yes, this isn't good, but ProtectedData in mono is throwing exceptions, so use this for now
+
+ var bytes = Convert.FromBase64String(value);
+ return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Security/MBLicenseFile.cs b/Emby.Server.Implementations/Security/MBLicenseFile.cs
new file mode 100644
index 000000000..c791d6a52
--- /dev/null
+++ b/Emby.Server.Implementations/Security/MBLicenseFile.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Security
+{
+ internal class MBLicenseFile
+ {
+ private readonly IApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly ICryptoProvider _cryptographyProvider;
+
+ public string RegKey
+ {
+ get { return _regKey; }
+ set
+ {
+ if (value != _regKey)
+ {
+ //if key is changed - clear out our saved validations
+ _updateRecords.Clear();
+ _regKey = value;
+ }
+ }
+ }
+
+ private string Filename
+ {
+ get
+ {
+ return Path.Combine(_appPaths.ConfigurationDirectoryPath, "mb.lic");
+ }
+ }
+
+ private readonly ConcurrentDictionary<Guid, FeatureRegInfo> _updateRecords = new ConcurrentDictionary<Guid, FeatureRegInfo>();
+ private readonly object _fileLock = new object();
+ private string _regKey;
+
+ public MBLicenseFile(IApplicationPaths appPaths, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
+ {
+ _appPaths = appPaths;
+ _fileSystem = fileSystem;
+ _cryptographyProvider = cryptographyProvider;
+
+ Load();
+ }
+
+ private void SetUpdateRecord(Guid key, FeatureRegInfo value)
+ {
+ _updateRecords.AddOrUpdate(key, value, (k, v) => value);
+ }
+
+ private Guid GetKey(string featureId)
+ {
+ return new Guid(_cryptographyProvider.ComputeMD5(Encoding.Unicode.GetBytes(featureId)));
+ }
+
+ public void AddRegCheck(string featureId, DateTime expirationDate)
+ {
+ var key = GetKey(featureId);
+ var value = new FeatureRegInfo
+ {
+ ExpirationDate = expirationDate,
+ LastChecked = DateTime.UtcNow
+ };
+
+ SetUpdateRecord(key, value);
+ Save();
+ }
+
+ public void RemoveRegCheck(string featureId)
+ {
+ var key = GetKey(featureId);
+ FeatureRegInfo val;
+
+ _updateRecords.TryRemove(key, out val);
+
+ Save();
+ }
+
+ public FeatureRegInfo GetRegInfo(string featureId)
+ {
+ var key = GetKey(featureId);
+ FeatureRegInfo info = null;
+ _updateRecords.TryGetValue(key, out info);
+
+ if (info == null)
+ {
+ return null;
+ }
+
+ // guard agains people just putting a large number in the file
+ return info.LastChecked < DateTime.UtcNow ? info : null;
+ }
+
+ private void Load()
+ {
+ string[] contents = null;
+ var licenseFile = Filename;
+ lock (_fileLock)
+ {
+ try
+ {
+ contents = _fileSystem.ReadAllLines(licenseFile);
+ }
+ catch (FileNotFoundException)
+ {
+ lock (_fileLock)
+ {
+ _fileSystem.WriteAllBytes(licenseFile, new byte[] { });
+ }
+ }
+ catch (IOException)
+ {
+ lock (_fileLock)
+ {
+ _fileSystem.WriteAllBytes(licenseFile, new byte[] { });
+ }
+ }
+ }
+ if (contents != null && contents.Length > 0)
+ {
+ //first line is reg key
+ RegKey = contents[0];
+
+ //next is legacy key
+ if (contents.Length > 1)
+ {
+ // Don't need this anymore
+ }
+
+ //the rest of the lines should be pairs of features and timestamps
+ for (var i = 2; i < contents.Length; i = i + 2)
+ {
+ var line = contents[i];
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ Guid feat;
+ if (Guid.TryParse(line, out feat))
+ {
+ var lineParts = contents[i + 1].Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+
+ long ticks;
+ if (long.TryParse(lineParts[0], out ticks))
+ {
+ var info = new FeatureRegInfo
+ {
+ LastChecked = new DateTime(ticks)
+ };
+
+ if (lineParts.Length > 1 && long.TryParse(lineParts[1], out ticks))
+ {
+ info.ExpirationDate = new DateTime(ticks);
+ }
+
+ SetUpdateRecord(feat, info);
+ }
+ }
+ }
+ }
+ }
+
+ public void Save()
+ {
+ //build our array
+ var lines = new List<string>
+ {
+ RegKey,
+
+ // Legacy key
+ string.Empty
+ };
+
+ foreach (var pair in _updateRecords
+ .ToList())
+ {
+ lines.Add(pair.Key.ToString());
+
+ var dateLine = pair.Value.LastChecked.Ticks.ToString(CultureInfo.InvariantCulture) + "|" +
+ pair.Value.ExpirationDate.Ticks.ToString(CultureInfo.InvariantCulture);
+
+ lines.Add(dateLine);
+ }
+
+ var licenseFile = Filename;
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(licenseFile));
+ lock (_fileLock)
+ {
+ _fileSystem.WriteAllLines(licenseFile, lines);
+ }
+ }
+ }
+
+ internal class FeatureRegInfo
+ {
+ public DateTime ExpirationDate { get; set; }
+ public DateTime LastChecked { get; set; }
+
+ public FeatureRegInfo()
+ {
+ ExpirationDate = DateTime.MinValue;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Security/PluginSecurityManager.cs b/Emby.Server.Implementations/Security/PluginSecurityManager.cs
new file mode 100644
index 000000000..f21259137
--- /dev/null
+++ b/Emby.Server.Implementations/Security/PluginSecurityManager.cs
@@ -0,0 +1,355 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Security;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Serialization;
+
+namespace Emby.Server.Implementations.Security
+{
+ /// <summary>
+ /// Class PluginSecurityManager
+ /// </summary>
+ public class PluginSecurityManager : ISecurityManager
+ {
+ private const string MBValidateUrl = "https://mb3admin.com/admin/service/registration/validate";
+ private const string AppstoreRegUrl = /*MbAdmin.HttpsUrl*/ "https://mb3admin.com/admin/service/appstore/register";
+
+ /// <summary>
+ /// The _is MB supporter
+ /// </summary>
+ private bool? _isMbSupporter;
+ /// <summary>
+ /// The _is MB supporter initialized
+ /// </summary>
+ private bool _isMbSupporterInitialized;
+ /// <summary>
+ /// The _is MB supporter sync lock
+ /// </summary>
+ private object _isMbSupporterSyncLock = new object();
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is MB supporter.
+ /// </summary>
+ /// <value><c>true</c> if this instance is MB supporter; otherwise, <c>false</c>.</value>
+ public bool IsMBSupporter
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _isMbSupporter, ref _isMbSupporterInitialized, ref _isMbSupporterSyncLock, () => GetSupporterRegistrationStatus().Result.IsRegistered);
+ return _isMbSupporter.Value;
+ }
+ }
+
+ private MBLicenseFile _licenseFile;
+ private MBLicenseFile LicenseFile
+ {
+ get { return _licenseFile ?? (_licenseFile = new MBLicenseFile(_appPaths, _fileSystem, _cryptographyProvider)); }
+ }
+
+ private readonly IHttpClient _httpClient;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IServerApplicationHost _appHost;
+ private readonly ILogger _logger;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IFileSystem _fileSystem;
+ private readonly ICryptoProvider _cryptographyProvider;
+
+ private IEnumerable<IRequiresRegistration> _registeredEntities;
+ protected IEnumerable<IRequiresRegistration> RegisteredEntities
+ {
+ get
+ {
+ return _registeredEntities ?? (_registeredEntities = _appHost.GetExports<IRequiresRegistration>());
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginSecurityManager" /> class.
+ /// </summary>
+ public PluginSecurityManager(IServerApplicationHost appHost, IHttpClient httpClient, IJsonSerializer jsonSerializer,
+ IApplicationPaths appPaths, ILogManager logManager, IFileSystem fileSystem, ICryptoProvider cryptographyProvider)
+ {
+ if (httpClient == null)
+ {
+ throw new ArgumentNullException("httpClient");
+ }
+
+ _appHost = appHost;
+ _httpClient = httpClient;
+ _jsonSerializer = jsonSerializer;
+ _appPaths = appPaths;
+ _fileSystem = fileSystem;
+ _cryptographyProvider = cryptographyProvider;
+ _logger = logManager.GetLogger("SecurityManager");
+ }
+
+ /// <summary>
+ /// Load all registration info for all entities that require registration
+ /// </summary>
+ /// <returns></returns>
+ public async Task LoadAllRegistrationInfo()
+ {
+ var tasks = new List<Task>();
+
+ ResetSupporterInfo();
+ tasks.AddRange(RegisteredEntities.Select(i => i.LoadRegistrationInfoAsync()));
+ await Task.WhenAll(tasks);
+ }
+
+ /// <summary>
+ /// Gets the registration status.
+ /// This overload supports existing plug-ins.
+ /// </summary>
+ /// <param name="feature">The feature.</param>
+ /// <param name="mb2Equivalent">The MB2 equivalent.</param>
+ /// <returns>Task{MBRegistrationRecord}.</returns>
+ public Task<MBRegistrationRecord> GetRegistrationStatus(string feature, string mb2Equivalent = null)
+ {
+ return GetRegistrationStatusInternal(feature, mb2Equivalent);
+ }
+
+ /// <summary>
+ /// Gets the registration status.
+ /// </summary>
+ /// <param name="feature">The feature.</param>
+ /// <param name="mb2Equivalent">The MB2 equivalent.</param>
+ /// <param name="version">The version of this feature</param>
+ /// <returns>Task{MBRegistrationRecord}.</returns>
+ public Task<MBRegistrationRecord> GetRegistrationStatus(string feature, string mb2Equivalent, string version)
+ {
+ return GetRegistrationStatusInternal(feature, mb2Equivalent, version);
+ }
+
+ private Task<MBRegistrationRecord> GetSupporterRegistrationStatus()
+ {
+ return GetRegistrationStatusInternal("MBSupporter", null, _appHost.ApplicationVersion.ToString());
+ }
+
+ /// <summary>
+ /// Gets or sets the supporter key.
+ /// </summary>
+ /// <value>The supporter key.</value>
+ public string SupporterKey
+ {
+ get
+ {
+ return LicenseFile.RegKey;
+ }
+ set
+ {
+ var newValue = value;
+ if (newValue != null)
+ {
+ newValue = newValue.Trim();
+ }
+
+ if (newValue != LicenseFile.RegKey)
+ {
+ LicenseFile.RegKey = newValue;
+ LicenseFile.Save();
+
+ // re-load registration info
+ Task.Run(() => LoadAllRegistrationInfo());
+ }
+ }
+ }
+
+ /// <summary>
+ /// Register an app store sale with our back-end. It will validate the transaction with the store
+ /// and then register the proper feature and then fill in the supporter key on success.
+ /// </summary>
+ /// <param name="parameters">Json parameters to send to admin server</param>
+ public async Task RegisterAppStoreSale(string parameters)
+ {
+ var options = new HttpRequestOptions()
+ {
+ Url = AppstoreRegUrl,
+ CancellationToken = CancellationToken.None,
+ BufferContent = false
+ };
+ options.RequestHeaders.Add("X-Emby-Token", _appHost.SystemId);
+ options.RequestContent = parameters;
+ options.RequestContentType = "application/json";
+
+ try
+ {
+ using (var response = await _httpClient.Post(options).ConfigureAwait(false))
+ {
+ var reg = _jsonSerializer.DeserializeFromStream<RegRecord>(response.Content);
+
+ if (reg == null)
+ {
+ var msg = "Result from appstore registration was null.";
+ _logger.Error(msg);
+ throw new ArgumentException(msg);
+ }
+ if (!String.IsNullOrEmpty(reg.key))
+ {
+ SupporterKey = reg.key;
+ }
+ }
+
+ }
+ catch (ArgumentException)
+ {
+ SaveAppStoreInfo(parameters);
+ throw;
+ }
+ catch (HttpException e)
+ {
+ _logger.ErrorException("Error registering appstore purchase {0}", e, parameters ?? "NO PARMS SENT");
+
+ if (e.StatusCode.HasValue && e.StatusCode.Value == HttpStatusCode.PaymentRequired)
+ {
+ throw new PaymentRequiredException();
+ }
+ throw new Exception("Error registering store sale");
+ }
+ catch (Exception e)
+ {
+ _logger.ErrorException("Error registering appstore purchase {0}", e, parameters ?? "NO PARMS SENT");
+ SaveAppStoreInfo(parameters);
+ //TODO - could create a re-try routine on start-up if this file is there. For now we can handle manually.
+ throw new Exception("Error registering store sale");
+ }
+
+ }
+
+ private void SaveAppStoreInfo(string info)
+ {
+ // Save all transaction information to a file
+
+ try
+ {
+ _fileSystem.WriteAllText(Path.Combine(_appPaths.ProgramDataPath, "apptrans-error.txt"), info);
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+
+ private async Task<MBRegistrationRecord> GetRegistrationStatusInternal(string feature,
+ string mb2Equivalent = null,
+ string version = null)
+ {
+ var regInfo = LicenseFile.GetRegInfo(feature);
+ var lastChecked = regInfo == null ? DateTime.MinValue : regInfo.LastChecked;
+ var expDate = regInfo == null ? DateTime.MinValue : regInfo.ExpirationDate;
+
+ var maxCacheDays = 14;
+ var nextCheckDate = new [] { expDate, lastChecked.AddDays(maxCacheDays) }.Min();
+
+ if (nextCheckDate > DateTime.UtcNow.AddDays(maxCacheDays))
+ {
+ nextCheckDate = DateTime.MinValue;
+ }
+
+ //check the reg file first to alleviate strain on the MB admin server - must actually check in every 30 days tho
+ var reg = new RegRecord
+ {
+ // Cache the result for up to a week
+ registered = regInfo != null && nextCheckDate >= DateTime.UtcNow && expDate >= DateTime.UtcNow,
+ expDate = expDate
+ };
+
+ var success = reg.registered;
+
+ if (!(lastChecked > DateTime.UtcNow.AddDays(-1)) || !reg.registered)
+ {
+ var data = new Dictionary<string, string>
+ {
+ { "feature", feature },
+ { "key", SupporterKey },
+ { "mac", _appHost.SystemId },
+ { "systemid", _appHost.SystemId },
+ { "mb2equiv", mb2Equivalent },
+ { "ver", version },
+ { "platform", _appHost.OperatingSystemDisplayName },
+ { "isservice", _appHost.IsRunningAsService.ToString().ToLower() }
+ };
+
+ try
+ {
+ var options = new HttpRequestOptions
+ {
+ Url = MBValidateUrl,
+
+ // Seeing block length errors
+ EnableHttpCompression = false,
+ BufferContent = false
+ };
+
+ options.SetPostData(data);
+
+ using (var json = (await _httpClient.Post(options).ConfigureAwait(false)).Content)
+ {
+ reg = _jsonSerializer.DeserializeFromStream<RegRecord>(json);
+ success = true;
+ }
+
+ if (reg.registered)
+ {
+ LicenseFile.AddRegCheck(feature, reg.expDate);
+ }
+ else
+ {
+ LicenseFile.RemoveRegCheck(feature);
+ }
+
+ }
+ catch (Exception e)
+ {
+ _logger.ErrorException("Error checking registration status of {0}", e, feature);
+ }
+ }
+
+ var record = new MBRegistrationRecord
+ {
+ IsRegistered = reg.registered,
+ ExpirationDate = reg.expDate,
+ RegChecked = true,
+ RegError = !success
+ };
+
+ record.TrialVersion = IsInTrial(reg.expDate, record.RegChecked, record.IsRegistered);
+ record.IsValid = !record.RegChecked || record.IsRegistered || record.TrialVersion;
+
+ return record;
+ }
+
+ private bool IsInTrial(DateTime expirationDate, bool regChecked, bool isRegistered)
+ {
+ //don't set this until we've successfully obtained exp date
+ if (!regChecked)
+ {
+ return false;
+ }
+
+ var isInTrial = expirationDate > DateTime.UtcNow;
+
+ return isInTrial && !isRegistered;
+ }
+
+ /// <summary>
+ /// Resets the supporter info.
+ /// </summary>
+ private void ResetSupporterInfo()
+ {
+ _isMbSupporter = null;
+ _isMbSupporterInitialized = false;
+ }
+ }
+} \ No newline at end of file
diff --git a/Emby.Server.Implementations/Security/RegRecord.cs b/Emby.Server.Implementations/Security/RegRecord.cs
new file mode 100644
index 000000000..d484085d3
--- /dev/null
+++ b/Emby.Server.Implementations/Security/RegRecord.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Emby.Server.Implementations.Security
+{
+ class RegRecord
+ {
+ public string featId { get; set; }
+ public bool registered { get; set; }
+ public DateTime expDate { get; set; }
+ public string key { get; set; }
+ }
+} \ No newline at end of file