mirror of
https://github.com/mRemoteNG/mRemoteNG.git
synced 2026-02-17 14:07:46 +08:00
Introducing Local settings manager class and example of default settings json schema
Idea to create json file with defaults settings, what will be automatically loaded into new local settings db at creating process. Group 'default' means its comes from json file and if user change it later - we will have user id there, that allow us to have different settings for different users but at same time keeping "original" ones in case user decide to reset. What one of the basic steps of transition all settings into local db. Once application is running - will be possible to import already existing settings into local db, as well as switch where to have save them: to windows registery or to local db. Settings file also could be encrypted, and file will be locked on current machine, that allow all users to use it, but not allow to transfer it to another machine. (If they do it will not work) mR don't use any hard-coded passwords for such, only hdd id + machine name (what is hashed by SHA256) so couldn't be traceable from memory dump. Later will be possible to lock by user with own password, but that will required to enter on every start as mR will not save it.
This commit is contained in:
@@ -37,24 +37,10 @@ namespace mRemoteNG.App
|
||||
}
|
||||
return null;
|
||||
};
|
||||
/*
|
||||
* Temporarily disable LocalSettingsManager initialization at startup
|
||||
* due to unfinished implementation causing build errors.
|
||||
* Uncomment if needed in your local repo.
|
||||
*/
|
||||
/*
|
||||
/*var settingsManager = new LocalSettingsManager();
|
||||
|
||||
// Check if the database exists
|
||||
if (settingsManager.DatabaseExists())
|
||||
{
|
||||
Console.WriteLine("Database exists.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Database does not exist. Creating...");
|
||||
settingsManager.CreateDatabase();
|
||||
}*/
|
||||
LocalSettingsDBManager settingsManager = new LocalSettingsDBManager(dbPath: "mRemoteNG.appSettings", useEncryption: false, schemaFilePath: "");
|
||||
|
||||
|
||||
|
||||
if (Properties.OptionsStartupExitPage.Default.SingleInstance)
|
||||
StartApplicationAsSingleInstance();
|
||||
|
||||
417
mRemoteNG/Config/Settings/LocalSettingsManager.cs
Normal file
417
mRemoteNG/Config/Settings/LocalSettingsManager.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Management;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
using LiteDB;
|
||||
using System.Linq;
|
||||
|
||||
public class LocalSettingsDBManager
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
private readonly string _schemaPath;
|
||||
private readonly string _mRIdentifier;
|
||||
private readonly bool? _useEncryption;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new local DB, encrypt it or decrypt it.
|
||||
/// </summary>
|
||||
/// <param name="dbPath">The path to the database file.</param>
|
||||
/// <param name="useEncryption">Indicates whether to use encryption for the database. If null, no change is made to an existing database.</param>
|
||||
/// <param name="schemaFilePath">Optional path to a schema file for creating the database structure.</param>
|
||||
public LocalSettingsDBManager(string dbPath = null, bool? useEncryption = null, string schemaFilePath = null)
|
||||
{
|
||||
_dbPath = string.IsNullOrWhiteSpace(dbPath) ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mRemoteNG.appSettings") : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbPath);
|
||||
_schemaPath = string.IsNullOrWhiteSpace(schemaFilePath) ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Schemas\\mremoteng_default_settings_v1_0.json") : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, schemaFilePath);
|
||||
_useEncryption = useEncryption;
|
||||
_mRIdentifier = Convert.ToBase64String(System.Security.Cryptography.SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(GetDiskIdentifier() + "_" + Environment.MachineName)));
|
||||
|
||||
// Check if disk identifier is empty and prevent database creation if true
|
||||
if (string.IsNullOrEmpty(_mRIdentifier))
|
||||
{
|
||||
Console.WriteLine("Calculated identifier is empty. Database creation aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the database exists and handle accordingly
|
||||
if (!File.Exists(_dbPath))
|
||||
{
|
||||
CreateDatabase(_schemaPath);
|
||||
}
|
||||
else if (_useEncryption.HasValue)
|
||||
{
|
||||
if (_useEncryption.Value)
|
||||
{
|
||||
EncryptDatabase();
|
||||
}
|
||||
else
|
||||
{
|
||||
DecryptDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures default settings are imported if the database is empty.
|
||||
/// </summary>
|
||||
/// <param name="importFilePath">Path to the JSON file for importing default settings.</param>
|
||||
public void EnsureDefaultSettingsImported(string importFilePath)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
if (db.GetCollectionNames().All(name => db.GetCollection<Setting>(name).Count() == 0))
|
||||
{
|
||||
Console.WriteLine("No settings found in database. Importing default settings...");
|
||||
ImportSettings(importFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Database already contains settings. Skipping import.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the database is encrypted.
|
||||
/// </summary>
|
||||
/// <returns>True if the database is encrypted, otherwise false.</returns>
|
||||
private bool IsDatabaseEncrypted()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var db = new LiteDatabase($"Filename={_dbPath}"))
|
||||
{
|
||||
// If we can open the database without a password, it means it is not encrypted.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (LiteException)
|
||||
{
|
||||
// If an exception is thrown, it means the database is likely encrypted.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the database using the machine identifier as a password if encryption is enabled.
|
||||
/// </summary>
|
||||
/// <param name="schemaFilePath">Path to the schema file for creating the database structure.</param>
|
||||
private void CreateDatabase(string schemaFilePath = null)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(schemaFilePath) && File.Exists(schemaFilePath))
|
||||
{
|
||||
var schemaJson = File.ReadAllText(schemaFilePath);
|
||||
using (JsonDocument doc = JsonDocument.Parse(schemaJson))
|
||||
{
|
||||
foreach (JsonElement table in doc.RootElement.GetProperty("tables").EnumerateArray())
|
||||
{
|
||||
string tableName = table.GetProperty("name").GetString();
|
||||
var collection = db.GetCollection<Setting>(tableName);
|
||||
Console.WriteLine($"Table '{tableName}' created with structure from schema.");
|
||||
|
||||
// Insert default data into the collection if defined in the schema
|
||||
if (table.TryGetProperty("columns", out JsonElement columnsElement))
|
||||
{
|
||||
foreach (JsonElement column in columnsElement.EnumerateArray())
|
||||
{
|
||||
var settingsData = new Setting
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Group = "default",
|
||||
Key = column.GetProperty("name").GetString(),
|
||||
Value = column.GetProperty("value").ToString()
|
||||
};
|
||||
collection.Insert(settingsData);
|
||||
Console.WriteLine($"Inserted default setting '{settingsData.Key}' for table '{tableName}'.");
|
||||
}
|
||||
};
|
||||
Console.WriteLine($"Inserted default settings for table '{tableName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Console.WriteLine(_useEncryption.HasValue && _useEncryption.Value ? "Database created and encrypted." : "Database created without encryption.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts an existing database if it is not encrypted.
|
||||
/// </summary>
|
||||
public void EncryptDatabase()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var db = new LiteDatabase($"Filename={_dbPath}"))
|
||||
{
|
||||
Console.WriteLine("Encrypting database...");
|
||||
var backupPath = _dbPath + ".backup";
|
||||
db.Checkpoint();
|
||||
File.Copy(_dbPath, backupPath, true);
|
||||
|
||||
using (var encryptedDb = new LiteDatabase($"Filename={_dbPath};Password={_mRIdentifier}"))
|
||||
{
|
||||
encryptedDb.Checkpoint();
|
||||
}
|
||||
|
||||
File.Delete(backupPath);
|
||||
Console.WriteLine("Database successfully encrypted.");
|
||||
}
|
||||
}
|
||||
catch (LiteException ex)
|
||||
{
|
||||
Console.WriteLine($"Error encrypting database: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts an existing database if it is encrypted.
|
||||
/// </summary>
|
||||
public void DecryptDatabase()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsDatabaseEncrypted())
|
||||
{
|
||||
Console.WriteLine("Database is not encrypted. Skipping decryption.");
|
||||
return;
|
||||
}
|
||||
var encryptedConnectionString = $"Filename={_dbPath};Password={_mRIdentifier}";
|
||||
using (var db = new LiteDatabase(encryptedConnectionString))
|
||||
{
|
||||
Console.WriteLine("Decrypting database...");
|
||||
var backupPath = _dbPath + ".backup";
|
||||
db.Checkpoint();
|
||||
File.Copy(_dbPath, backupPath, true);
|
||||
|
||||
using (var decryptedDb = new LiteDatabase($"Filename={_dbPath}"))
|
||||
{
|
||||
decryptedDb.Checkpoint();
|
||||
}
|
||||
|
||||
File.Delete(backupPath);
|
||||
Console.WriteLine("Database successfully decrypted.");
|
||||
}
|
||||
}
|
||||
catch (LiteException ex)
|
||||
{
|
||||
Console.WriteLine($"Error decrypting database: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new setting to the database.
|
||||
/// </summary>
|
||||
/// <param name="table">Table name.</param>
|
||||
/// <param name="group">Setting group.</param>
|
||||
/// <param name="key">Setting key.</param>
|
||||
/// <param name="value">Setting value.</param>
|
||||
public void AddSetting(string table, string group, string key, string value)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
var settings = db.GetCollection<Setting>(table);
|
||||
var setting = new Setting
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Group = group,
|
||||
Key = key,
|
||||
Value = value
|
||||
};
|
||||
settings.Insert(setting);
|
||||
Console.WriteLine($"Setting '{group}.{key}' added to table '{table}'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports settings from a JSON file into the database.
|
||||
/// </summary>
|
||||
/// <param name="jsonFilePath">Path to the JSON file.</param>
|
||||
public void ImportSettings(string jsonFilePath)
|
||||
{
|
||||
if (File.Exists(jsonFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(jsonFilePath);
|
||||
var settingsData = JsonSerializer.Deserialize<Dictionary<string, List<Setting>>>(json);
|
||||
|
||||
foreach (var table in settingsData.Keys)
|
||||
{
|
||||
foreach (var setting in settingsData[table])
|
||||
{
|
||||
AddSetting(table, setting.Group, setting.Key, setting.Value);
|
||||
}
|
||||
}
|
||||
Console.WriteLine("Settings successfully imported from JSON file.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("JSON file not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports settings from the database to a JSON file.
|
||||
/// </summary>
|
||||
/// <param name="jsonFilePath">Path to the JSON file.</param>
|
||||
public void ExportSettings(string jsonFilePath)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
var settingsData = new Dictionary<string, List<Setting>>();
|
||||
|
||||
foreach (var tableName in db.GetCollectionNames())
|
||||
{
|
||||
var settings = db.GetCollection<Setting>(tableName).FindAll();
|
||||
settingsData[tableName] = new List<Setting>(settings);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(settingsData, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(jsonFilePath, json);
|
||||
Console.WriteLine("Settings successfully exported to JSON file.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of a setting by table, group, and key.
|
||||
/// </summary>
|
||||
/// <param name="table">Table name.</param>
|
||||
/// <param name="group">Setting group.</param>
|
||||
/// <param name="key">Setting key.</param>
|
||||
/// <returns>Setting value or "Not Found" if the setting does not exist.</returns>
|
||||
public string GetSetting(string table, string group, string key)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
var settings = db.GetCollection<Setting>(table);
|
||||
var setting = settings.FindOne(s => s.Group == group && s.Key == key);
|
||||
return setting != null ? setting.Value : "Not Found";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the value of an existing setting and updates the timestamp.
|
||||
/// </summary>
|
||||
/// <param name="table">Table name.</param>
|
||||
/// <param name="group">Setting group.</param>
|
||||
/// <param name="key">Setting key.</param>
|
||||
/// <param name="newValue">New value for the setting.</param>
|
||||
public void UpdateSetting(string table, string group, string key, string newValue)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
var settings = db.GetCollection<Setting>(table);
|
||||
var setting = settings.FindOne(s => s.Group == group && s.Key == key);
|
||||
if (setting != null)
|
||||
{
|
||||
setting.Value = newValue;
|
||||
setting.Timestamp = DateTime.UtcNow;
|
||||
settings.Update(setting);
|
||||
Console.WriteLine($"Setting '{group}.{key}' updated in table '{table}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Setting '{group}.{key}' not found in table '{table}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a setting by table, group, and key.
|
||||
/// </summary>
|
||||
/// <param name="table">Table name.</param>
|
||||
/// <param name="group">Setting group.</param>
|
||||
/// <param name="key">Setting key.</param>
|
||||
public void DeleteSetting(string table, string group, string key)
|
||||
{
|
||||
var connectionString = _useEncryption.HasValue && _useEncryption.Value
|
||||
? $"Filename={_dbPath};Password={_mRIdentifier}"
|
||||
: $"Filename={_dbPath}";
|
||||
|
||||
using (var db = new LiteDatabase(connectionString))
|
||||
{
|
||||
var settings = db.GetCollection<Setting>(table);
|
||||
if (settings.DeleteMany(s => s.Group == group && s.Key == key) > 0)
|
||||
{
|
||||
Console.WriteLine($"Setting '{group}.{key}' deleted from table '{table}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Setting '{group}.{key}' not found in table '{table}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique machine identifier (serial number of the hard drive) combined with the machine name and encrypts it using SHA256.
|
||||
/// </summary>
|
||||
/// <returns>Unique machine identifier.</returns>
|
||||
private static string GetDiskIdentifier()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use ManagementObject to get the serial number of the hard drive
|
||||
using (var searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_DiskDrive"))
|
||||
{
|
||||
foreach (var disk in searcher.Get())
|
||||
{
|
||||
return disk["SerialNumber"].ToString().Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error getting disk identifier: {ex.Message}");
|
||||
throw new InvalidOperationException("Failed to retrieve disk identifier. Please ensure the disk information is accessible.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("This method is only supported on Windows.");
|
||||
}
|
||||
|
||||
// Return an empty string if no serial number is found
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Setting class
|
||||
public class Setting
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string Group { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ using System.Resources;
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Version information
|
||||
[assembly: AssemblyVersion("1.77.3.2693")]
|
||||
[assembly: AssemblyFileVersion("1.77.3.2693")]
|
||||
[assembly: AssemblyVersion("1.77.3.2694")]
|
||||
[assembly: AssemblyFileVersion("1.77.3.2694")]
|
||||
[assembly: NeutralResourcesLanguageAttribute("en-US")]
|
||||
[assembly: AssemblyInformationalVersion("1.77.3 (Nightly Build 2693)")]
|
||||
[assembly: AssemblyInformationalVersion("1.77.3 (Nightly Build 2694)")]
|
||||
|
||||
// Logging
|
||||
[assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config")]
|
||||
|
||||
88
mRemoteNG/Schemas/mremoteng_default_settings_v1_0.json
Normal file
88
mRemoteNG/Schemas/mremoteng_default_settings_v1_0.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||||
"title": "mRemoteNG Default Settings",
|
||||
"description": "This schema defines the default settings for mRemoteNG.",
|
||||
"type": "object",
|
||||
"database": {
|
||||
"filename": "mRemoteNG.appSettings",
|
||||
"autoCreatedb": true
|
||||
},
|
||||
"tables": [
|
||||
{
|
||||
"name": "main",
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the application.",
|
||||
"type": "string",
|
||||
"value": "mRemoteNG",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"description": "The version of the application.",
|
||||
"type": "string",
|
||||
"value": "1.77.3",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"columns": [
|
||||
{
|
||||
"name": "checkInterval",
|
||||
"description": "The interval at which to check for updates in seconds.",
|
||||
"type": "integer",
|
||||
"value": 86400,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "autoUpdate",
|
||||
"description": "Whether to automatically update the application.",
|
||||
"type": "boolean",
|
||||
"value": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "theme",
|
||||
"columns": [
|
||||
{
|
||||
"name": "primaryColor",
|
||||
"description": "The primary color of the application.",
|
||||
"type": "string",
|
||||
"value": "#[a-f0-9]{6}",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "secondaryColor",
|
||||
"type": "string",
|
||||
"value": "#[a-f0-9]{6}",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "backup",
|
||||
"columns": [
|
||||
{
|
||||
"name": "schedule",
|
||||
"description": "The schedule for backing up the application.",
|
||||
"type": "string",
|
||||
"value": ["daily", "weekly", "monthly"],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "destination",
|
||||
"description": "The destination for the backup file.",
|
||||
"type": "string",
|
||||
"format": "file",
|
||||
"value": "C:\\Users\\user\\Documents\\mRemoteNG\\backup",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -464,6 +464,9 @@
|
||||
<None Update="Schemas\mremoteng_creds_v1_0.xsd">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Schemas\mremoteng_default_settings_v1_0.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Themes\darcula.vstheme">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
||||
Reference in New Issue
Block a user