Added some helpers, a new logger and log migration and modified the router

This commit is contained in:
Marcel Baumgartner 2023-10-13 21:08:56 +02:00
parent 69b50275cd
commit afb3a7f3a3
10 changed files with 666 additions and 13 deletions

View file

@ -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<string> strings)
{
foreach (string str in strings)
{
if (suffix.EndsWith(str))
{
return true;
}
}
return false;
}
public static bool ContainsOneOf(string textToSearch, IEnumerable<string> 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<string> 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, "<br/>");
}
}
};
}
// 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;
}
}

View file

@ -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
{
/// <summary>
/// The default number of Iterations
/// </summary>
private const int DefaultIterations = 10000;
/// <summary>
/// Provides Information about a specific Hash Version
/// </summary>
private class HashVersion
{
public short Version { get; set; }
public int SaltSize { get; set; }
public int HashSize { get; set; }
public KeyDerivationPrf KeyDerivation { get; set; }
}
/// <summary>
/// Holds all possible Hash Versions
/// </summary>
private static readonly Dictionary<short, HashVersion> _versions = new Dictionary<short, HashVersion>
{
{
1, new HashVersion
{
Version = 1,
KeyDerivation = KeyDerivationPrf.HMACSHA512,
HashSize = 256 / 8,
SaltSize = 128 / 8
}
}
};
/// <summary>
/// The default Hash Version, which should be used, if a new Hash is Created
/// </summary>
private static HashVersion DefaultVersion => _versions[1];
/// <summary>
/// Checks if a given hash uses the latest version
/// </summary>
/// <param name="data">The hash</param>
/// <returns>Is the hash of the latest version?</returns>
public static bool IsLatestHashVersion(byte[] data)
{
var version = BitConverter.ToInt16(data, 0);
return version == DefaultVersion.Version;
}
/// <summary>
/// Checks if a given hash uses the latest version
/// </summary>
/// <param name="data">The hash</param>
/// <returns>Is the hash of the latest version?</returns>
public static bool IsLatestHashVersion(string data)
{
var dataBytes = Convert.FromBase64String(data);
return IsLatestHashVersion(dataBytes);
}
/// <summary>
/// Gets a random byte array
/// </summary>
/// <param name="length">The length of the byte array</param>
/// <returns>The random byte array</returns>
public static byte[] GetRandomBytes(int length)
{
var data = new byte[length];
using (var randomNumberGenerator = RandomNumberGenerator.Create())
{
randomNumberGenerator.GetBytes(data);
}
return data;
}
/// <summary>
/// Creates a Hash of a clear text
/// </summary>
/// <param name="clearText">the clear text</param>
/// <param name="iterations">the number of iteration the hash alogrythm should run</param>
/// <returns>the Hash</returns>
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;
}
/// <summary>
/// Creates a Hash of a clear text and convert it to a Base64 String representation
/// </summary>
/// <param name="clearText">the clear text</param>
/// <param name="iterations">the number of iteration the hash alogrythm should run</param>
/// <returns>the Hash</returns>
public static string HashToString(string clearText, int iterations = DefaultIterations)
{
var data = Hash(clearText, iterations);
return Convert.ToBase64String(data);
}
/// <summary>
/// Verifies a given clear Text against a hash
/// </summary>
/// <param name="clearText">The clear text</param>
/// <param name="data">The hash</param>
/// <returns>Is the hash equal to the clear text?</returns>
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);
}
/// <summary>
/// Verifies a given clear Text against a hash
/// </summary>
/// <param name="clearText">The clear text</param>
/// <param name="data">The hash</param>
/// <returns>Is the hash equal to the clear text?</returns>
public static bool Verify(string clearText, string data)
{
var dataBytes = Convert.FromBase64String(data);
return Verify(clearText, dataBytes);
}
}

View file

@ -0,0 +1,11 @@
namespace Moonlight.App.Helpers.LogMigrator;
public class LogMigrateProvider : ILoggerProvider
{
public void Dispose() {}
public ILogger CreateLogger(string categoryName)
{
return new MigrateLogger();
}
}

View file

@ -0,0 +1,53 @@
namespace Moonlight.App.Helpers.LogMigrator;
public class MigrateLogger : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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;
}
}
}

View file

@ -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;
}
}

View file

@ -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}"
);
}
}

View file

@ -2,7 +2,9 @@
<Router AppAssembly="@typeof(BlazorApp).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<CascadingValue TValue="Type" Name="TargetPageType" Value="routeData.PageType">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</CascadingValue>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>

View file

@ -19,7 +19,6 @@
<Folder Include="App\Database\Enums\" />
<Folder Include="App\Database\Migrations\" />
<Folder Include="App\Exceptions\" />
<Folder Include="App\Helpers\" />
<Folder Include="App\Http\" />
<Folder Include="App\Models\Abstractions\" />
<Folder Include="App\Models\Enums\" />
@ -28,4 +27,10 @@
<Folder Include="App\Services\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.1.0-dev-02078" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0-dev-00923" />
</ItemGroup>
</Project>

View file

@ -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();

View file

@ -2,4 +2,5 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Moonlight
@using Moonlight.App.Helpers
@using Moonlight.Shared.Components.Partials