From afb3a7f3a3f44a51ff978772b87d07b4c7b4b4eb Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Fri, 13 Oct 2023 21:08:56 +0200 Subject: [PATCH] Added some helpers, a new logger and log migration and modified the router --- Moonlight/App/Helpers/Formatter.cs | 252 ++++++++++++++++++ Moonlight/App/Helpers/HashHelper.cs | 172 ++++++++++++ .../Helpers/LogMigrator/LogMigrateProvider.cs | 11 + .../App/Helpers/LogMigrator/MigrateLogger.cs | 53 ++++ Moonlight/App/Helpers/Logger.cs | 112 ++++++++ Moonlight/App/Helpers/PathBuilder.cs | 34 +++ Moonlight/BlazorApp.razor | 4 +- Moonlight/Moonlight.csproj | 7 +- Moonlight/Program.cs | 33 ++- Moonlight/_Imports.razor | 1 + 10 files changed, 666 insertions(+), 13 deletions(-) create mode 100644 Moonlight/App/Helpers/Formatter.cs create mode 100644 Moonlight/App/Helpers/HashHelper.cs create mode 100644 Moonlight/App/Helpers/LogMigrator/LogMigrateProvider.cs create mode 100644 Moonlight/App/Helpers/LogMigrator/MigrateLogger.cs create mode 100644 Moonlight/App/Helpers/Logger.cs create mode 100644 Moonlight/App/Helpers/PathBuilder.cs diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs new file mode 100644 index 00000000..3b9c8417 --- /dev/null +++ b/Moonlight/App/Helpers/Formatter.cs @@ -0,0 +1,252 @@ +using System.Text; +using Microsoft.AspNetCore.Components; + +namespace Moonlight.App.Helpers; + +public static class Formatter +{ + public static string GenerateString(int length) + { + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var stringBuilder = new StringBuilder(); + var random = new Random(); + + for (int i = 0; i < length; i++) + { + stringBuilder.Append(chars[random.Next(chars.Length)]); + } + + return stringBuilder.ToString(); + } + + public static string IntToStringWithLeadingZeros(int number, int n) + { + string result = number.ToString(); + int length = result.Length; + + for (int i = length; i < n; i++) + { + result = "0" + result; + } + + return result; + } + + public static string CapitalizeFirstCharacter(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + char firstChar = char.ToUpper(input[0]); + string restOfString = input.Substring(1); + + return firstChar + restOfString; + } + + public static string CutInHalf(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + int length = input.Length; + int halfLength = length / 2; + + return input.Substring(0, halfLength); + } + + public static bool EndsInOneOf(string suffix, IEnumerable strings) + { + foreach (string str in strings) + { + if (suffix.EndsWith(str)) + { + return true; + } + } + + return false; + } + + public static bool ContainsOneOf(string textToSearch, IEnumerable strings, out string foundText) + { + foreach (string str in strings) + { + if (textToSearch.Contains(str)) + { + foundText = str; + return true; + } + } + + foundText = ""; + return false; + } + + public static bool ContainsOneOf(string textToSearch, IEnumerable strings) + { + return ContainsOneOf(textToSearch, strings, out _); + } + + public static string FormatSize(long bytes) + { + var i = Math.Abs(bytes) / 1024D; + if (i < 1) + { + return bytes + " B"; + } + else if (i / 1024D < 1) + { + return i.Round(2) + " KB"; + } + else if (i / (1024D * 1024D) < 1) + { + return (i / 1024D).Round(2) + " MB"; + } + else + { + return (i / (1024D * 1024D)).Round(2) + " GB"; + } + } + + private static double Round(this double d, int decimals) + { + return Math.Round(d, decimals); + } + + public static string ReplaceEnd(string input, string substringToReplace, string newSubstring) + { + int lastIndexOfSubstring = input.LastIndexOf(substringToReplace); + if (lastIndexOfSubstring >= 0) + { + input = input.Remove(lastIndexOfSubstring, substringToReplace.Length) + .Insert(lastIndexOfSubstring, newSubstring); + } + + return input; + } + + public static string ConvertCamelCaseToSpaces(string input) + { + StringBuilder output = new StringBuilder(); + + foreach (char c in input) + { + if (char.IsUpper(c)) + { + output.Append(' '); + } + + output.Append(c); + } + + return output.ToString().Trim(); + } + + public static string FormatUptime(double uptime) + { + TimeSpan t = TimeSpan.FromMilliseconds(uptime); + + return FormatUptime(t); + } + + public static string FormatUptime(TimeSpan t) + { + if (t.Days > 0) + { + return $"{t.Days}d {t.Hours}h {t.Minutes}m {t.Seconds}s"; + } + else + { + return $"{t.Hours}h {t.Minutes}m {t.Seconds}s"; + } + } + + public static string FormatDate(DateTime e) + { + string i2s(int i) + { + if (i.ToString().Length < 2) + return "0" + i; + return i.ToString(); + } + + return $"{i2s(e.Day)}.{i2s(e.Month)}.{e.Year} {i2s(e.Hour)}:{i2s(e.Minute)}"; + } + + public static string FormatDateOnly(DateTime e) + { + string i2s(int i) + { + if (i.ToString().Length < 2) + return "0" + i; + return i.ToString(); + } + + return $"{i2s(e.Day)}.{i2s(e.Month)}.{e.Year}"; + } + + public static string FormatSize(double bytes) + { + var i = Math.Abs(bytes) / 1024D; + if (i < 1) + { + return bytes + " B"; + } + else if (i / 1024D < 1) + { + return i.Round(2) + " KB"; + } + else if (i / (1024D * 1024D) < 1) + { + return (i / 1024D).Round(2) + " MB"; + } + else + { + return (i / (1024D * 1024D)).Round(2) + " GB"; + } + } + + public static RenderFragment FormatLineBreaks(string content) + { + return builder => + { + int i = 0; + var arr = content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (var line in arr) + { + builder.AddContent(i, line); + if (i++ != arr.Length - 1) + { + builder.AddMarkupContent(i, "
"); + } + } + }; + } + + // This will replace every placeholder with the respective value if specified in the model + // For example: + // A instance of the user model has been passed in the 'models' parameter of the function. + // So the placeholder {{User.Email}} will be replaced by the value of the Email property of the model + public static string ProcessTemplating(string text, params object[] models) + { + foreach (var model in models) + { + foreach (var property in model.GetType().GetProperties()) + { + var value = property.GetValue(model); + + if(value == null) + continue; + + var placeholder = "{{" + $"{model.GetType().Name}.{property.Name}" + "}}"; + + text = text.Replace(placeholder, value.ToString()); + } + } + + return text; + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/HashHelper.cs b/Moonlight/App/Helpers/HashHelper.cs new file mode 100644 index 00000000..97458b83 --- /dev/null +++ b/Moonlight/App/Helpers/HashHelper.cs @@ -0,0 +1,172 @@ +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace Moonlight.App.Helpers; + +// Src: https://codereview.stackexchange.com/questions/176697/net-core-mvc-future-proof-hashing-of-passwords +public static class HashHelper +{ + /// + /// The default number of Iterations + /// + private const int DefaultIterations = 10000; + + /// + /// Provides Information about a specific Hash Version + /// + private class HashVersion + { + public short Version { get; set; } + public int SaltSize { get; set; } + public int HashSize { get; set; } + public KeyDerivationPrf KeyDerivation { get; set; } + } + + /// + /// Holds all possible Hash Versions + /// + private static readonly Dictionary _versions = new Dictionary + { + { + 1, new HashVersion + { + Version = 1, + KeyDerivation = KeyDerivationPrf.HMACSHA512, + HashSize = 256 / 8, + SaltSize = 128 / 8 + } + } + }; + + /// + /// The default Hash Version, which should be used, if a new Hash is Created + /// + private static HashVersion DefaultVersion => _versions[1]; + + /// + /// Checks if a given hash uses the latest version + /// + /// The hash + /// Is the hash of the latest version? + public static bool IsLatestHashVersion(byte[] data) + { + var version = BitConverter.ToInt16(data, 0); + return version == DefaultVersion.Version; + } + + /// + /// Checks if a given hash uses the latest version + /// + /// The hash + /// Is the hash of the latest version? + public static bool IsLatestHashVersion(string data) + { + var dataBytes = Convert.FromBase64String(data); + return IsLatestHashVersion(dataBytes); + } + + /// + /// Gets a random byte array + /// + /// The length of the byte array + /// The random byte array + public static byte[] GetRandomBytes(int length) + { + var data = new byte[length]; + using (var randomNumberGenerator = RandomNumberGenerator.Create()) + { + randomNumberGenerator.GetBytes(data); + } + + return data; + } + + /// + /// Creates a Hash of a clear text + /// + /// the clear text + /// the number of iteration the hash alogrythm should run + /// the Hash + public static byte[] Hash(string clearText, int iterations = DefaultIterations) + { + //get current version + var currentVersion = DefaultVersion; + + //get the byte arrays of the hash and meta information + var saltBytes = GetRandomBytes(currentVersion.SaltSize); + var versionBytes = BitConverter.GetBytes(currentVersion.Version); + var iterationBytes = BitConverter.GetBytes(iterations); + var hashBytes = KeyDerivation.Pbkdf2(clearText, saltBytes, currentVersion.KeyDerivation, iterations, + currentVersion.HashSize); + + //calculate the indexes for the combined hash + var indexVersion = 0; + var indexIteration = indexVersion + 2; + var indexSalt = indexIteration + 4; + var indexHash = indexSalt + currentVersion.SaltSize; + + //combine all data to one result hash + var resultBytes = new byte[2 + 4 + currentVersion.SaltSize + currentVersion.HashSize]; + Array.Copy(versionBytes, 0, resultBytes, indexVersion, 2); + Array.Copy(iterationBytes, 0, resultBytes, indexIteration, 4); + Array.Copy(saltBytes, 0, resultBytes, indexSalt, currentVersion.SaltSize); + Array.Copy(hashBytes, 0, resultBytes, indexHash, currentVersion.HashSize); + return resultBytes; + } + + /// + /// Creates a Hash of a clear text and convert it to a Base64 String representation + /// + /// the clear text + /// the number of iteration the hash alogrythm should run + /// the Hash + public static string HashToString(string clearText, int iterations = DefaultIterations) + { + var data = Hash(clearText, iterations); + return Convert.ToBase64String(data); + } + + /// + /// Verifies a given clear Text against a hash + /// + /// The clear text + /// The hash + /// Is the hash equal to the clear text? + public static bool Verify(string clearText, byte[] data) + { + //Get the current version and number of iterations + var currentVersion = _versions[BitConverter.ToInt16(data, 0)]; + var iteration = BitConverter.ToInt32(data, 2); + + //Create the byte arrays for the salt and hash + var saltBytes = new byte[currentVersion.SaltSize]; + var hashBytes = new byte[currentVersion.HashSize]; + + //Calculate the indexes of the salt and the hash + var indexSalt = 2 + 4; // Int16 (Version) and Int32 (Iteration) + var indexHash = indexSalt + currentVersion.SaltSize; + + //Fill the byte arrays with salt and hash + Array.Copy(data, indexSalt, saltBytes, 0, currentVersion.SaltSize); + Array.Copy(data, indexHash, hashBytes, 0, currentVersion.HashSize); + + //Hash the current clearText with the parameters given via the data + var verificationHashBytes = KeyDerivation.Pbkdf2(clearText, saltBytes, currentVersion.KeyDerivation, iteration, + currentVersion.HashSize); + + //Check if generated hashes are equal + return hashBytes.SequenceEqual(verificationHashBytes); + } + + /// + /// Verifies a given clear Text against a hash + /// + /// The clear text + /// The hash + /// Is the hash equal to the clear text? + public static bool Verify(string clearText, string data) + { + var dataBytes = Convert.FromBase64String(data); + return Verify(clearText, dataBytes); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/LogMigrator/LogMigrateProvider.cs b/Moonlight/App/Helpers/LogMigrator/LogMigrateProvider.cs new file mode 100644 index 00000000..5c2469f6 --- /dev/null +++ b/Moonlight/App/Helpers/LogMigrator/LogMigrateProvider.cs @@ -0,0 +1,11 @@ +namespace Moonlight.App.Helpers.LogMigrator; + +public class LogMigrateProvider : ILoggerProvider +{ + public void Dispose() {} + + public ILogger CreateLogger(string categoryName) + { + return new MigrateLogger(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/LogMigrator/MigrateLogger.cs b/Moonlight/App/Helpers/LogMigrator/MigrateLogger.cs new file mode 100644 index 00000000..fb9100d1 --- /dev/null +++ b/Moonlight/App/Helpers/LogMigrator/MigrateLogger.cs @@ -0,0 +1,53 @@ +namespace Moonlight.App.Helpers.LogMigrator; + +public class MigrateLogger : ILogger +{ + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + switch (logLevel) + { + case LogLevel.Critical: + Logger.Fatal(formatter(state, exception)); + + if(exception != null) + Logger.Fatal(exception); + + break; + case LogLevel.Warning: + Logger.Warn(formatter(state, exception)); + + if(exception != null) + Logger.Warn(exception); + + break; + case LogLevel.Debug: + Logger.Debug(formatter(state, exception)); + + if(exception != null) + Logger.Debug(exception); + + break; + case LogLevel.Error: + Logger.Error(formatter(state, exception)); + + if(exception != null) + Logger.Error(exception); + + break; + case LogLevel.Information: + Logger.Info(formatter(state, exception)); + + if(exception != null) + Logger.Info(exception); + + break; + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/Logger.cs b/Moonlight/App/Helpers/Logger.cs new file mode 100644 index 00000000..efce9e9e --- /dev/null +++ b/Moonlight/App/Helpers/Logger.cs @@ -0,0 +1,112 @@ +using System.Diagnostics; +using System.Reflection; +using Serilog; + +namespace Moonlight.App.Helpers; + +public class Logger +{ + #region String logger + public static void Verbose(string message, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Verbose("{Message}", message); + } + + public static void Info(string message, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Information("{Message}", message); + } + + public static void Debug(string message, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Debug("{Message}", message); + } + + public static void Error(string message, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Error("{Message}", message); + } + + public static void Warn(string message, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Warning("{Message}", message); + } + + public static void Fatal(string message, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Fatal("{Message}", message); + } + + #endregion + + #region Exception method calls + + public static void Verbose(Exception exception, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Verbose(exception, ""); + } + + public static void Info(Exception exception, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Information(exception, ""); + } + + public static void Debug(Exception exception, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Debug(exception, ""); + } + + public static void Error(Exception exception, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Error(exception, ""); + } + + public static void Warn(Exception exception, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Warning(exception, ""); + } + + public static void Fatal(Exception exception, string channel = "default") + { + Log.ForContext("SourceContext", GetNameOfCallingClass()) + .Fatal(exception, ""); + } + + #endregion + + private static string GetNameOfCallingClass(int skipFrames = 4) + { + string fullName; + Type declaringType; + + do + { + MethodBase method = new StackFrame(skipFrames, false).GetMethod(); + declaringType = method.DeclaringType; + if (declaringType == null) + { + return method.Name; + } + + skipFrames++; + if (declaringType.Name.Contains("<")) + fullName = declaringType.ReflectedType.Name; + else + fullName = declaringType.Name; + } while (declaringType.Module.Name.Equals("mscorlib.dll", StringComparison.OrdinalIgnoreCase) | + fullName.Contains("Logger")); + + return fullName; + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/PathBuilder.cs b/Moonlight/App/Helpers/PathBuilder.cs new file mode 100644 index 00000000..8d1054cb --- /dev/null +++ b/Moonlight/App/Helpers/PathBuilder.cs @@ -0,0 +1,34 @@ +namespace Moonlight.App.Helpers; + +public static class PathBuilder +{ + public static string Dir(params string[] parts) + { + var res = ""; + + foreach (var part in parts) + { + res += part + Path.DirectorySeparatorChar; + } + + return res.Replace( + $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", + $"{Path.DirectorySeparatorChar}" + ); + } + + public static string File(params string[] parts) + { + var res = ""; + + foreach (var part in parts) + { + res += part + (part == parts.Last() ? "" : Path.DirectorySeparatorChar); + } + + return res.Replace( + $"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", + $"{Path.DirectorySeparatorChar}" + ); + } +} \ No newline at end of file diff --git a/Moonlight/BlazorApp.razor b/Moonlight/BlazorApp.razor index 385264d5..58302966 100644 --- a/Moonlight/BlazorApp.razor +++ b/Moonlight/BlazorApp.razor @@ -2,7 +2,9 @@ - + + + Not found diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 622dce0e..0564111b 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -19,7 +19,6 @@ - @@ -28,4 +27,10 @@ + + + + + + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index a3e47a64..fd8d3507 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -1,10 +1,29 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; using Moonlight.App.Extensions; +using Moonlight.App.Helpers; +using Moonlight.App.Helpers.LogMigrator; +using Serilog; + +Directory.CreateDirectory(PathBuilder.Dir("storage")); +Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); + +var logConfig = new LoggerConfiguration(); + +logConfig = logConfig.Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: + "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"); + +Log.Logger = logConfig.CreateLogger(); var builder = WebApplication.CreateBuilder(args); + builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddControllers(); + +builder.Logging.ClearProviders(); +builder.Logging.AddProvider(new LogMigrateProvider()); var config = new ConfigurationBuilder().AddJsonString( @@ -13,19 +32,11 @@ builder.Logging.AddConfiguration(config.Build()); var app = builder.Build(); -if (!app.Environment.IsDevelopment()) -{ - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} - -app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); +app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/Moonlight/_Imports.razor b/Moonlight/_Imports.razor index 3c109dca..a621e56e 100644 --- a/Moonlight/_Imports.razor +++ b/Moonlight/_Imports.razor @@ -2,4 +2,5 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop @using Moonlight +@using Moonlight.App.Helpers @using Moonlight.Shared.Components.Partials \ No newline at end of file