diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1c1e2ea..fb564c4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,7 +12,7 @@ jobs: os: - "windows-latest" - "ubuntu-latest" - #- "macos-latest" + - "macos-latest" dotnet: - "8.0" runs-on: ${{ matrix.os }} @@ -37,9 +37,6 @@ jobs: - name: "Construct files" run: | cp -r E5Renewer/bin/Release/net${{ matrix.dotnet }}/publish dist - mkdir -p dist/modules - cp -r E5Renewer.Modules.TomlParser/bin/Release/net${{ matrix.dotnet }}/publish dist/modules/E5Renewer.Modules.TomlParser - cp -r E5Renewer.Modules.YamlParser/bin/Release/net${{ matrix.dotnet }}/publish dist/modules/E5Renewer.Modules.YamlParser - name: "Create archive" run: "7z a E5Renewer-${{ steps.build-env-info.outputs.system }}-${{ steps.build-env-info.outputs.machine }}.7z ./dist/*" diff --git a/Directory.Build.props b/Directory.Build.props index 70825f6..99303cd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.1.0 + 0.2.0 true true true diff --git a/E5Renewer.Modules.TomlParser.Tests/E5Renewer.Modules.TomlParser.Tests.csproj b/E5Renewer.Modules.TomlParser.Tests/E5Renewer.Modules.TomlParser.Tests.csproj deleted file mode 100644 index 840ebf0..0000000 --- a/E5Renewer.Modules.TomlParser.Tests/E5Renewer.Modules.TomlParser.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/E5Renewer.Modules.TomlParser.Tests/GlobalUsings.cs b/E5Renewer.Modules.TomlParser.Tests/GlobalUsings.cs deleted file mode 100644 index 540383d..0000000 --- a/E5Renewer.Modules.TomlParser.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/E5Renewer.Modules.TomlParser.Tests/TomlParserTests.cs b/E5Renewer.Modules.TomlParser.Tests/TomlParserTests.cs deleted file mode 100644 index d169690..0000000 --- a/E5Renewer.Modules.TomlParser.Tests/TomlParserTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace E5Renewer.Modules.TomlParser.Tests; - -/// Test -/// -/// -[TestClass] -public class TomlParserTests -{ - private readonly TomlParser parser = new(); - - /// Ensure name. - [TestMethod] - public void TestName() => Assert.AreEqual("TomlParser", this.parser.name); - /// Ensure author. - [TestMethod] - public void TestAuthor() => Assert.AreEqual("E5Renewer", this.parser.author); - - /// Test - /// - /// - [TestMethod] - [DataRow("test.toml", true)] - [DataRow("test.yaml", false)] - [DataRow("test.json", false)] - public void TestIsSupported(string path, bool result) => Assert.AreEqual(result, this.parser.IsSupported(path)); - - /// Test - /// - /// - [TestMethod] - [DataRow("", false)] - [DataRow("auth_token = \"test-token\"", true)] - public async Task TestParseConfigAsyncAuthToken(string json, bool result) - { - string tmpPath = Path.GetTempFileName(); - await File.WriteAllTextAsync(tmpPath, json); - E5Renewer.Models.Config.Config config = await this.parser.ParseConfigAsync(tmpPath); - bool compareResult = config.authToken == "test-token"; - Assert.AreEqual(result, compareResult); - } -} diff --git a/E5Renewer.Modules.TomlParser/E5Renewer.Modules.TomlParser.csproj b/E5Renewer.Modules.TomlParser/E5Renewer.Modules.TomlParser.csproj deleted file mode 100644 index 77fab2f..0000000 --- a/E5Renewer.Modules.TomlParser/E5Renewer.Modules.TomlParser.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - enable - enable - true - - - - - - - - - - false - runtime - - - - diff --git a/E5Renewer.Modules.TomlParser/TomlParser.cs b/E5Renewer.Modules.TomlParser/TomlParser.cs deleted file mode 100644 index bdf168b..0000000 --- a/E5Renewer.Modules.TomlParser/TomlParser.cs +++ /dev/null @@ -1,48 +0,0 @@ -using CaseConverter; - -using E5Renewer.Models; -using E5Renewer.Models.Config; -using E5Renewer.Models.Modules; - -using Tomlyn; - -namespace E5Renewer.Modules.TomlParser; - -/// -/// Parse toml to -/// Config. -/// -[Module] -public class TomlParser : IConfigParser -{ - /// - public string name { get => nameof(TomlParser); } - - /// - public string author { get => "E5Renewer"; } - - /// - public SemanticVersioning.Version apiVersion - { - get => typeof(TomlParser).Assembly.GetName().Version?.ToSemanticVersion() ?? new(0, 1, 0); - } - - /// - public bool IsSupported(string path) => path.EndsWith(".toml"); - - /// - public async ValueTask ParseConfigAsync(string path) - { - Config runtimeConfig; - using (StreamReader stream = File.OpenText(path)) - { - runtimeConfig = Toml.ToModel( - await stream.ReadToEndAsync(), path, new() - { - ConvertPropertyName = (input) => input.ToSnakeCase() - } - ); - } - return runtimeConfig; - } -} diff --git a/E5Renewer.Modules.YamlParser.Tests/E5Renewer.Modules.YamlParser.Tests.csproj b/E5Renewer.Modules.YamlParser.Tests/E5Renewer.Modules.YamlParser.Tests.csproj deleted file mode 100644 index 42c2431..0000000 --- a/E5Renewer.Modules.YamlParser.Tests/E5Renewer.Modules.YamlParser.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/E5Renewer.Modules.YamlParser.Tests/GlobalUsings.cs b/E5Renewer.Modules.YamlParser.Tests/GlobalUsings.cs deleted file mode 100644 index 540383d..0000000 --- a/E5Renewer.Modules.YamlParser.Tests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/E5Renewer.Modules.YamlParser.Tests/YamlParserTests.cs b/E5Renewer.Modules.YamlParser.Tests/YamlParserTests.cs deleted file mode 100644 index ce848ee..0000000 --- a/E5Renewer.Modules.YamlParser.Tests/YamlParserTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace E5Renewer.Modules.YamlParser.Tests; - -/// Test -/// -/// -[TestClass] -public class YamlParserTests -{ - private readonly YamlParser parser = new(); - - /// Ensure name. - [TestMethod] - public void TestName() => Assert.AreEqual("YamlParser", this.parser.name); - - /// Ensure author. - [TestMethod] - public void TestAuthor() => Assert.AreEqual("E5Renewer", this.parser.author); - - /// Test - /// - /// - [TestMethod] - [DataRow("test.toml", false)] - [DataRow("test.yaml", true)] - [DataRow("test.yml", true)] - [DataRow("test.json", false)] - public void TestIsSupported(string path, bool result) => Assert.AreEqual(result, this.parser.IsSupported(path)); - - /// Test - /// - /// - [TestMethod] - [DataRow("{}", false)] - [DataRow("auth_token: test-token", true)] - public async Task TestParseConfigAsyncAuthToken(string json, bool result) - { - string tmpPath = Path.GetTempFileName(); - await File.WriteAllTextAsync(tmpPath, json); - E5Renewer.Models.Config.Config config = await this.parser.ParseConfigAsync(tmpPath); - bool compareResult = config.authToken == "test-token"; - Assert.AreEqual(result, compareResult); - } -} diff --git a/E5Renewer.Modules.YamlParser/E5Renewer.Modules.YamlParser.csproj b/E5Renewer.Modules.YamlParser/E5Renewer.Modules.YamlParser.csproj deleted file mode 100644 index acdffda..0000000 --- a/E5Renewer.Modules.YamlParser/E5Renewer.Modules.YamlParser.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net8.0 - enable - enable - true - - - - - - - - - - false - runtime - - - - diff --git a/E5Renewer.Modules.YamlParser/YamlParser.cs b/E5Renewer.Modules.YamlParser/YamlParser.cs deleted file mode 100644 index 3446f5c..0000000 --- a/E5Renewer.Modules.YamlParser/YamlParser.cs +++ /dev/null @@ -1,44 +0,0 @@ -using E5Renewer.Models; -using E5Renewer.Models.Config; -using E5Renewer.Models.Modules; - -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace E5Renewer.Modules.YamlParser; - -/// -/// Parse yaml to -/// Config. -/// -[Module] -public class YamlParser : IConfigParser -{ - /// - public string name { get => nameof(YamlParser); } - - /// - public string author { get => "E5Renewer"; } - - /// - public SemanticVersioning.Version apiVersion - { - get => typeof(YamlParser).Assembly.GetName().Version?.ToSemanticVersion() ?? new(0, 1, 0); - } - - /// - public bool IsSupported(string path) => path.EndsWith(".yaml") || path.EndsWith(".yml"); - - /// - public async ValueTask ParseConfigAsync(string path) - { - Config runtimeConfig; - using (StreamReader stream = File.OpenText(path)) - { - IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build(); - runtimeConfig = deserializer.Deserialize(await stream.ReadToEndAsync()); - } - return runtimeConfig; - } - -} diff --git a/E5Renewer.Tests/Controllers/JsonAPIV1ControllerTests.cs b/E5Renewer.Tests/Controllers/JsonAPIV1ControllerTests.cs index 49942d4..d064ae0 100644 --- a/E5Renewer.Tests/Controllers/JsonAPIV1ControllerTests.cs +++ b/E5Renewer.Tests/Controllers/JsonAPIV1ControllerTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; +using Microsoft.AspNetCore.Http; namespace E5Renewer.Tests.Controllers; @@ -30,11 +31,18 @@ public JsonAPIV1ControllerTests() IAPIFunction apiFunction = Substitute.For(); apiFunction.id.Returns("test"); - List apiFunctions = [apiFunction]; - IUnixTimestampGenerator generator = new UnixTimestampGenerator(); - this.controller = new(logger, statusManager, apiFunctions, generator); + IUnixTimestampGenerator generator = Substitute.For(); + generator.GetUnixTimestamp().Returns((long)42); + + IDummyResultGenerator dummyResultGenerator = Substitute.For(); + InvokeResult dummyResult = new(); + HttpContext context = new DefaultHttpContext(); + dummyResultGenerator.GenerateDummyResultAsync(context).ReturnsForAnyArgs(dummyResult); + dummyResultGenerator.GenerateDummyResult(context).ReturnsForAnyArgs(dummyResult); + + this.controller = new(logger, statusManager, apiFunctions, generator, dummyResultGenerator); } /// Test /// @@ -85,4 +93,13 @@ public async Task TestGetUserResults() string? status = ((IEnumerable?)result.result)?.First(); Assert.AreEqual("200 - OK", status); } + /// Test + /// + /// + [TestMethod] + public async Task TestHandle() + { + InvokeResult result = await this.controller.Handle(); + Assert.AreEqual(new(), result); + } } diff --git a/E5Renewer.Tests/Controllers/SimpleDummyResultGeneratorTests.cs b/E5Renewer.Tests/Controllers/SimpleDummyResultGeneratorTests.cs new file mode 100644 index 0000000..e035299 --- /dev/null +++ b/E5Renewer.Tests/Controllers/SimpleDummyResultGeneratorTests.cs @@ -0,0 +1,47 @@ +using E5Renewer.Controllers; +using E5Renewer.Models.Statistics; + +using NSubstitute; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; + +namespace E5Renewer.Tests.Controllers; + +/// Test +/// +/// +[TestClass] +public class SimpleDummyResultGeneratorTests +{ + private readonly SimpleDummyResultGenerator dummyResultGenerator; + + /// Initialize with no argument. + public SimpleDummyResultGeneratorTests() + { + ILogger logger = Substitute.For>(); + IUnixTimestampGenerator timestampGenerator = Substitute.For(); + timestampGenerator.GetUnixTimestamp().ReturnsForAnyArgs((long)42); + this.dummyResultGenerator = new(logger, timestampGenerator); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGenerateDummyResultAsync() + { + HttpContext context = new DefaultHttpContext(); + InvokeResult result = await this.dummyResultGenerator.GenerateDummyResultAsync(context); + Assert.AreEqual((long)42, result.timestamp); + } + /// Test + /// + /// + [TestMethod] + public void TestGenerateDummyResult() + { + HttpContext context = new DefaultHttpContext(); + InvokeResult result = this.dummyResultGenerator.GenerateDummyResult(context); + Assert.AreEqual((long)42, result.timestamp); + } +} diff --git a/E5Renewer.Tests/Controllers/UnspecifiedControllerTests.cs b/E5Renewer.Tests/Controllers/UnspecifiedControllerTests.cs new file mode 100644 index 0000000..a915215 --- /dev/null +++ b/E5Renewer.Tests/Controllers/UnspecifiedControllerTests.cs @@ -0,0 +1,40 @@ +using E5Renewer.Controllers; + +using NSubstitute; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; + +namespace E5Renewer.Tests.Controllers; + +/// Test +/// +/// +[TestClass] +public class UnspecifiedControllerTests +{ + private readonly UnspecifiedController controller; + + /// Initialize with no argument. + public UnspecifiedControllerTests() + { + ILogger logger = Substitute.For>(); + IDummyResultGenerator dummyResultGenerator = Substitute.For(); + InvokeResult result = new(); + HttpContext context = new DefaultHttpContext(); + dummyResultGenerator.GenerateDummyResultAsync(context).Returns(Task.FromResult(result)); + dummyResultGenerator.GenerateDummyResult(context).Returns(result); + this.controller = new(logger, dummyResultGenerator); + this.controller.ControllerContext.HttpContext = context; + } + + /// Test + /// + /// + [TestMethod] + public async Task TestHandle() + { + InvokeResult result = await this.controller.Handle(); + Assert.AreEqual(new(), result); + } +} + diff --git a/E5Renewer.Tests/Models/Config/ConfigCertificatePasswordProviderTests.cs b/E5Renewer.Tests/Models/Config/ConfigCertificatePasswordProviderTests.cs deleted file mode 100644 index 8b46f43..0000000 --- a/E5Renewer.Tests/Models/Config/ConfigCertificatePasswordProviderTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Security.Cryptography; - -using E5Renewer.Models.Config; - -using Microsoft.Extensions.Logging; - -using NSubstitute; - -namespace E5Renewer.Tests.Models.Config; - -/// Test -/// -/// -[TestClass] -public class ConfigCertificatePasswordProviderTests -{ - private readonly string tmpFilePath = Path.GetTempFileName(); - private readonly ConfigCertificatePasswordProvider provider; - - /// Initialize with no argument. - public ConfigCertificatePasswordProviderTests() - { - Random random = new(); - byte[] buffer = new byte[1024]; - random.NextBytes(buffer); - byte[] hash = SHA512.HashData(buffer); - string hashString = BitConverter.ToString(hash).Replace("-", "").ToLower(); - Dictionary passwords = new() - { - {hashString, "example-secret"} - }; - using (Stream stream = File.OpenWrite(this.tmpFilePath)) - { - stream.Write(buffer); - } - ILogger logger = Substitute.For>(); - this.provider = new(logger, passwords); - } - /// Test - /// - /// - [TestMethod] - public async Task TestGetPasswordForCertificateAsync() - { - string? password = await this.provider.GetPasswordForCertificateAsync(this.tmpFilePath); - Assert.AreEqual("example-secret", password); - } -} diff --git a/E5Renewer.Tests/Models/Config/JsonConfigParserTests.cs b/E5Renewer.Tests/Models/Config/JsonConfigParserTests.cs deleted file mode 100644 index 8ecbef3..0000000 --- a/E5Renewer.Tests/Models/Config/JsonConfigParserTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using E5Renewer.Models.Config; - -namespace E5Renewer.Tests.Models.Config; - -/// Test -/// -/// -[TestClass] -public class JsonConfigParserTests -{ - private readonly JsonConfigParser parser = new(); - - /// Ensure name. - [TestMethod] - public void TestName() => Assert.AreEqual("JsonConfigParser", this.parser.name); - - /// Ensure author. - [TestMethod] - public void TestAuthor() => Assert.AreEqual("E5Renewer", this.parser.author); - - /// Test - /// - /// - [TestMethod] - [DataRow("test.toml", false)] - [DataRow("test.yaml", false)] - [DataRow("test.json", true)] - public void TestIsSupported(string path, bool result) => Assert.AreEqual(result, this.parser.IsSupported(path)); - - /// Test - /// - /// - [TestMethod] - [DataRow("{}", false)] - [DataRow("{\"auth_token\": \"test-token\"}", true)] - public async Task TestParseConfigAsyncAuthToken(string json, bool result) - { - string tmpPath = Path.GetTempFileName(); - await File.WriteAllTextAsync(tmpPath, json); - E5Renewer.Models.Config.Config config = await this.parser.ParseConfigAsync(tmpPath); - bool compareResult = config.authToken == "test-token"; - Assert.AreEqual(result, compareResult); - } -} diff --git a/E5Renewer.Tests/Models/Modules/DeprecatedModulesCheckerTests.cs b/E5Renewer.Tests/Models/Modules/DeprecatedModulesCheckerTests.cs new file mode 100644 index 0000000..2e81898 --- /dev/null +++ b/E5Renewer.Tests/Models/Modules/DeprecatedModulesCheckerTests.cs @@ -0,0 +1,33 @@ +using E5Renewer.Models.Modules; + +using NSubstitute; +using Microsoft.Extensions.Logging; + +namespace E5Renewer.Tests.Models.Modules; + +/// Test +/// +/// +[TestClass] +public class DeprecatedModulesCheckerTests +{ + private readonly DeprecatedModulesChecker checker; + + /// Initialize with no argument. + public DeprecatedModulesCheckerTests() + { + ILogger logger = Substitute.For>(); + this.checker = new(logger); + } + + /// Test + /// + /// + [TestMethod] + public void TestCheckModules() + { + IModule module = Substitute.For(); + module.isDeprecated.Returns(false); + this.checker.CheckModules(module); + } +} diff --git a/E5Renewer.Tests/Models/Secrets/Json/JsonUserSecretLoaderTests.cs b/E5Renewer.Tests/Models/Secrets/Json/JsonUserSecretLoaderTests.cs new file mode 100644 index 0000000..b7a76e3 --- /dev/null +++ b/E5Renewer.Tests/Models/Secrets/Json/JsonUserSecretLoaderTests.cs @@ -0,0 +1,55 @@ +using E5Renewer.Models.Secrets; +using E5Renewer.Models.Secrets.Json; + +namespace E5Renewer.Tests.Models.Secrets.Json; + +/// Test +/// +/// +[TestClass] +public class JsonUserSecretLoaderTests : UserSecretLoaderTests +{ + private const string validJsonContentWithSecret = """{"users":[{"name":"test", "tenant_id":"test","client_id":"test","secret":"test"}]}"""; + private const string validJsonContentWithSecretAndDays = """{"users":[{"name":"test", "tenant_id":"test","client_id":"test","secret":"test", "days": [1]}]}"""; + + private const string invalidJsonContentWithCertificateNotExist = """{"users":[{"name":"test", "tenant_id":"test","client_id":"test","certificate":"not-exist"}]}"""; + private const string invalidJsonContentWithoutSecretOrCertificate = """{"users":[{"name":"test", "tenant_id":"test","client_id":"test"}]}"""; + + private readonly JsonUserSecretLoader loader; + + /// Initialize with no argument. + public JsonUserSecretLoaderTests() + { + this.loader = new(); + } + + /// Test + /// + /// + [TestMethod] + [DataRow("/path/to/json", false)] + [DataRow("C:\\json", false)] + [DataRow("/path/to/file.json", true)] + [DataRow("C:\\file.json", true)] + public override void TestIsSupported(string path, bool result) + { + FileInfo info = new(path); + bool actual = this.loader.IsSupported(info); + Assert.AreEqual(result, actual); + } + + /// Test + /// + /// + [TestMethod] + [DataRow(validJsonContentWithSecret, true)] + [DataRow(validJsonContentWithSecretAndDays, true)] + [DataRow(invalidJsonContentWithoutSecretOrCertificate, false)] + [DataRow(invalidJsonContentWithCertificateNotExist, false)] + public override async Task TestLoadSecretAsync(string jsonContent, bool expected) + { + FileInfo tempFile = await this.PrepareContent(jsonContent); + UserSecret secret = await this.loader.LoadSecretAsync(tempFile); + Assert.AreEqual(expected, secret.valid); + } +} diff --git a/E5Renewer.Tests/Models/Secrets/SimpleSecretProviderTests.cs b/E5Renewer.Tests/Models/Secrets/SimpleSecretProviderTests.cs new file mode 100644 index 0000000..5c80c0e --- /dev/null +++ b/E5Renewer.Tests/Models/Secrets/SimpleSecretProviderTests.cs @@ -0,0 +1,61 @@ +using E5Renewer.Models.Secrets; +using NSubstitute; +using Microsoft.Extensions.Logging; + +namespace E5Renewer.Tests.Models.Secrets; + +/// Test +/// +/// +[TestClass] +public class SimpleSecretProviderTests +{ + private readonly SimpleSecretProvider provider; + + /// Initialize with no argument. + public SimpleSecretProviderTests() + { + ILogger logger = Substitute.For>(); + FileInfo userSecret = new("test"); + IUserSecretLoader userSecretLoader = Substitute.For(); + UserSecret secret = new(); + userSecretLoader.IsSupported(userSecret).Returns(true); + userSecretLoader.LoadSecretAsync(userSecret).Returns(Task.FromResult(secret)); + IEnumerable userSecretLoaders = Substitute.For>(); + IUserSecretLoader[] userSecretLoaderArray = [userSecretLoader]; + userSecretLoaders.GetEnumerator().Returns(userSecretLoaderArray.ToList().GetEnumerator()); + this.provider = new(logger, userSecret, userSecretLoaders); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGetPasswordForCertificateAsync() + { + FileInfo noExist = new("no-exist"); + string? password = await this.provider.GetPasswordForCertificateAsync(noExist); + Assert.IsNull(password); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGetRuntimeTokenAsync() + { + string token = await this.provider.GetRuntimeTokenAsync(); + bool tokenNull = string.IsNullOrEmpty(token); + Assert.IsFalse(tokenNull); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGetUserSecretAsync() + { + UserSecret secret = await this.provider.GetUserSecretAsync(); + Assert.AreEqual(new(), secret); + } +} diff --git a/E5Renewer.Tests/Models/Secrets/Toml/TomlUserSecretLoaderTests.cs b/E5Renewer.Tests/Models/Secrets/Toml/TomlUserSecretLoaderTests.cs new file mode 100644 index 0000000..3245630 --- /dev/null +++ b/E5Renewer.Tests/Models/Secrets/Toml/TomlUserSecretLoaderTests.cs @@ -0,0 +1,80 @@ +using E5Renewer.Models.Secrets; +using E5Renewer.Models.Secrets.Toml; + +namespace E5Renewer.Tests.Models.Secrets.Toml; + +/// Test +/// +/// +[TestClass] +public class TomlUserSecretLoaderTests : UserSecretLoaderTests +{ + private const string validTomlContentWithSecret = + """ + [[users]] + name="test" + tenant_id="test" + client_id="test" + secret="test" + """; + private const string validTomlContentWithSecretAndDays = + """ + [[users]] + name="test" + tenant_id="test" + client_id="test" + secret="test" + days=[1] + """; + + private const string invalidTomlContentWithCertificateNotExist = + """ + [[users]] + name="test" + tenant_id="test" + client_id="test" + certificate="not-exist" + """; + private const string invalidTomlContentWithoutSecretOrCertificate = + """ + [[users]] + name="test" + tenant_id="test" + client_id="test" + """; + + private readonly TomlUserSecretLoader loader; + + /// Initialize with no argument. + public TomlUserSecretLoaderTests() => this.loader = new(); + + /// Test + /// + /// + [TestMethod] + [DataRow("/path/to/toml", false)] + [DataRow("C:\\toml", false)] + [DataRow("/path/to/file.toml", true)] + [DataRow("C:\\file.toml", true)] + public override void TestIsSupported(string path, bool result) + { + FileInfo info = new(path); + bool actual = this.loader.IsSupported(info); + Assert.AreEqual(result, actual); + } + + /// Test + /// + /// + [TestMethod] + [DataRow(validTomlContentWithSecret, true)] + [DataRow(validTomlContentWithSecretAndDays, true)] + [DataRow(invalidTomlContentWithoutSecretOrCertificate, false)] + [DataRow(invalidTomlContentWithCertificateNotExist, false)] + public override async Task TestLoadSecretAsync(string tomlContent, bool expected) + { + FileInfo tempFile = await this.PrepareContent(tomlContent); + UserSecret secret = await this.loader.LoadSecretAsync(tempFile); + Assert.AreEqual(expected, secret.valid); + } +} diff --git a/E5Renewer.Tests/Models/Secrets/UserSecretLoaderTests.cs b/E5Renewer.Tests/Models/Secrets/UserSecretLoaderTests.cs new file mode 100644 index 0000000..0a4e4c1 --- /dev/null +++ b/E5Renewer.Tests/Models/Secrets/UserSecretLoaderTests.cs @@ -0,0 +1,28 @@ +using E5Renewer.Models.Secrets; + +namespace E5Renewer.Tests.Models.Secrets; + +/// Base class to provide some utils for testing +/// implementations. +/// +public abstract class UserSecretLoaderTests +{ + /// Generate a temp file with content given. + /// The of temp file. + protected async Task PrepareContent(string content) + { + FileInfo tempFile = new(Path.GetTempFileName()); + using (StreamWriter streamWriter = tempFile.CreateText()) + { + await streamWriter.WriteAsync(content); + await streamWriter.FlushAsync(); + } + return tempFile; + } + + /// Test + public abstract void TestIsSupported(string path, bool result); + + /// Test + public abstract Task TestLoadSecretAsync(string jsonContent, bool expected); +} diff --git a/E5Renewer.Tests/Models/Secrets/Yaml/YamlUserSecretLoaderTests.cs b/E5Renewer.Tests/Models/Secrets/Yaml/YamlUserSecretLoaderTests.cs new file mode 100644 index 0000000..886591f --- /dev/null +++ b/E5Renewer.Tests/Models/Secrets/Yaml/YamlUserSecretLoaderTests.cs @@ -0,0 +1,85 @@ +using E5Renewer.Models.Secrets; +using E5Renewer.Models.Secrets.Yaml; + +namespace E5Renewer.Tests.Models.Secrets.Yaml; + +/// Test +/// +/// +[TestClass] +public class YamlUserSecretLoaderTests : UserSecretLoaderTests +{ + private const string validYamlContentWithSecret = + """ + users: + - name: test + tenant_id: test + client_id: test + secret: test + """; + private const string validYamlContentWithSecretAndDays = + """ + users: + - name: test + tenant_id: test + client_id: test + secret: test + days: + - 1 + """; + + private const string invalidYamlContentWithCertificateNotExist = + """ + users: + - name: test + tenant_id: test + client_id: test + certificate: not-exist + """; + private const string invalidYamlContentWithNoSecretOrCertificate = + """ + users: + - name: test + tenant_id: test + client_id: test + """; + + private readonly YamlUserSecretLoader loader; + + /// Initialize with no argument. + public YamlUserSecretLoaderTests() => this.loader = new(); + + /// Test + /// + /// + [TestMethod] + [DataRow("/path/to/yaml", false)] + [DataRow("/path/to/yml", false)] + [DataRow("C:\\yaml", false)] + [DataRow("C:\\yml", false)] + [DataRow("/path/to/file.yaml", true)] + [DataRow("/path/to/file.yml", true)] + [DataRow("C:\\file.yaml", true)] + [DataRow("C:\\file.yml", true)] + public override void TestIsSupported(string path, bool result) + { + FileInfo info = new(path); + bool actual = this.loader.IsSupported(info); + Assert.AreEqual(result, actual); + } + + /// Test + /// + /// + [TestMethod] + [DataRow(validYamlContentWithSecret, true)] + [DataRow(validYamlContentWithSecretAndDays, true)] + [DataRow(invalidYamlContentWithCertificateNotExist, false)] + [DataRow(invalidYamlContentWithNoSecretOrCertificate, false)] + public override async Task TestLoadSecretAsync(string yamlContent, bool expected) + { + FileInfo tempFile = await this.PrepareContent(yamlContent); + UserSecret secret = await this.loader.LoadSecretAsync(tempFile); + Assert.AreEqual(expected, secret.valid); + } +} diff --git a/E5Renewer.Tests/Models/Statistics/MemoryStatusManagerTests.cs b/E5Renewer.Tests/Models/Statistics/MemoryStatusManagerTests.cs new file mode 100644 index 0000000..d308229 --- /dev/null +++ b/E5Renewer.Tests/Models/Statistics/MemoryStatusManagerTests.cs @@ -0,0 +1,75 @@ +using E5Renewer.Models.Statistics; + +namespace E5Renewer.Tests.Models.Statistics; + +/// Test +/// +/// +[TestClass] +public class MemoryStatusManagerTests +{ + private readonly MemoryStatusManager manager; + + /// Initialize with no argument. + public MemoryStatusManagerTests() + { + this.manager = new(); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGetRunningUsersAsync() + { + IEnumerable runningUsers = await this.manager.GetRunningUsersAsync(); + int count = runningUsers.Count(); + Assert.AreEqual(0, count); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGetWaitingUsersAsync() + { + IEnumerable waitingUsers = await this.manager.GetWaitingUsersAsync(); + int count = waitingUsers.Count(); + Assert.AreEqual(0, count); + } + + /// Test + /// + /// + [TestMethod] + [DataRow("test-running", true, 1, 0)] + [DataRow("test-waiting", false, 0, 1)] + public async Task TestSetUserStatusAsync(string name, bool running, int targetRunningCount, int targetWaitingCount) + { + await this.manager.SetUserStatusAsync(name, running); + int runningCount = (await this.manager.GetRunningUsersAsync()).Count(); + int waitingCount = (await this.manager.GetWaitingUsersAsync()).Count(); + Assert.AreEqual(targetRunningCount, runningCount); + Assert.AreEqual(targetWaitingCount, waitingCount); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestGetResultsAsync() + { + IEnumerable results = await this.manager.GetResultsAsync("test", "Test.Example"); + int count = results.Count(); + Assert.AreEqual(0, count); + } + + /// Test + /// + /// + [TestMethod] + public async Task TestSetResultAsync() + { + await this.manager.SetResultAsync("test", "Test.Example.Set", "Success"); + } +} diff --git a/E5Renewer.Tests/Models/Statistics/UnixTimestampGeneratorTests.cs b/E5Renewer.Tests/Models/Statistics/UnixTimestampGeneratorTests.cs new file mode 100644 index 0000000..adee162 --- /dev/null +++ b/E5Renewer.Tests/Models/Statistics/UnixTimestampGeneratorTests.cs @@ -0,0 +1,29 @@ +using E5Renewer.Models.Statistics; + +namespace E5Renewer.Tests.Models.Statistics; + +/// Test +/// +/// +[TestClass] +public class UnixTimestampGeneratorTests +{ + private readonly UnixTimestampGenerator generator; + + /// Initialize with no argument. + public UnixTimestampGeneratorTests() + { + this.generator = new(); + } + + /// Test + /// + /// + [TestMethod] + public void TestGetUnixTimestamp() + { + long result1 = this.generator.GetUnixTimestamp(); + long result2 = this.generator.GetUnixTimestamp(); + Assert.AreNotSame(result1, result2); + } +} diff --git a/E5Renewer.Tests/Models/UintExtendsTests.cs b/E5Renewer.Tests/Models/UintExtendsTests.cs deleted file mode 100644 index 706df8f..0000000 --- a/E5Renewer.Tests/Models/UintExtendsTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using E5Renewer.Models; - -namespace E5Renewer.Tests.Models; - -/// Test -/// -/// -[TestClass] -public class UintExtendsTests -{ - /// - /// Test - /// - /// - [TestMethod] - [DataRow(0b000000000, UnixFileMode.None)] - [DataRow(0b000000001, UnixFileMode.OtherExecute)] - [DataRow(0b000000010, UnixFileMode.OtherWrite)] - [DataRow(0b000000011, UnixFileMode.OtherWrite | UnixFileMode.OtherExecute)] - [DataRow(0b000000100, UnixFileMode.OtherRead)] - [DataRow(0b000000101, UnixFileMode.OtherRead | UnixFileMode.OtherExecute)] - [DataRow(0b000000110, UnixFileMode.OtherRead | UnixFileMode.OtherWrite)] - [DataRow(0b000000111, UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute)] - [DataRow(0b000001000, UnixFileMode.GroupExecute)] - [DataRow(0b000010000, UnixFileMode.GroupWrite)] - [DataRow(0b000011000, UnixFileMode.GroupWrite | UnixFileMode.GroupExecute)] - [DataRow(0b000100000, UnixFileMode.GroupRead)] - [DataRow(0b000101000, UnixFileMode.GroupRead | UnixFileMode.GroupExecute)] - [DataRow(0b000110000, UnixFileMode.GroupRead | UnixFileMode.GroupWrite)] - [DataRow(0b000111000, UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute)] - [DataRow(0b001000000, UnixFileMode.UserExecute)] - [DataRow(0b010000000, UnixFileMode.UserWrite)] - [DataRow(0b011000000, UnixFileMode.UserWrite | UnixFileMode.UserExecute)] - [DataRow(0b100000000, UnixFileMode.UserRead)] - [DataRow(0b101000000, UnixFileMode.UserRead | UnixFileMode.UserExecute)] - [DataRow(0b110000000, UnixFileMode.UserRead | UnixFileMode.UserWrite)] - [DataRow(0b111000000, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute)] - [DataRow(0b100100100, UnixFileMode.UserRead | UnixFileMode.GroupRead | UnixFileMode.OtherRead)] - public void TestToUnixFileMode(int permission, UnixFileMode mode) - { - Assert.AreEqual(mode, ((uint)permission).ToUnixFileMode()); - } -} diff --git a/E5Renewer.Tests/TypeArrayExtendsTests.cs b/E5Renewer.Tests/TypeArrayExtendsTests.cs deleted file mode 100644 index 49e10c2..0000000 --- a/E5Renewer.Tests/TypeArrayExtendsTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace E5Renewer.Tests; - -/// -/// Test -/// -/// -[TestClass] -public class TypeArrayExtendsTests -{ - private void TestGetNonAbstractClassesAssainableToHelper(uint count) - { - IEnumerable typesFound = typeof(TypeArrayExtendsTests).Assembly.GetTypes().GetNonAbstractClassesAssainableTo(); - Assert.AreEqual((int)count, typesFound.Count()); - } - - /// - /// Test - /// - /// - [TestMethod] - public void TestGetNonAbstractClassesAssainableTo() - { - this.TestGetNonAbstractClassesAssainableToHelper(1); - } -} diff --git a/E5Renewer.Tests/WebApplicationExtendsTests.cs b/E5Renewer.Tests/WebApplicationExtendsTests.cs deleted file mode 100644 index 4348d28..0000000 --- a/E5Renewer.Tests/WebApplicationExtendsTests.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System.Net; -using System.Net.Http.Json; - -using E5Renewer.Models.Statistics; - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; - -namespace E5Renewer.Tests; - -/// Test -/// . -/// -[TestClass] -public class WebApplicationExtendsTests -{ - private static readonly Uri baseAddress = new("http://localhost:65530/"); - private const string requestUri = "/test"; - private static readonly Func responseAction = () => "OK"; - /// Test - /// - /// - [TestMethod] - [DataRow("example-auth-token-invalid", HttpStatusCode.Forbidden)] - [DataRow("example-auth-token", HttpStatusCode.OK)] - public async Task TestUseAuthTokenAuthentication(string token, HttpStatusCode target) - { - const string validAuthToken = "example-auth-token"; - - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseAuthTokenAuthentication(validAuthToken); - app.MapGet(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - client.DefaultRequestHeaders.Add("Authentication", token); - using (HttpResponseMessage response = await client.GetAsync(WebApplicationExtendsTests.requestUri)) - { - Assert.AreEqual(target, response.StatusCode); - } - await app.StopAsync(); - } - } - /// Test - /// - /// - [TestMethod] - public async Task TestUseAuthTokenAuthentication() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseAuthTokenAuthentication("example-auth-token"); - app.MapGet(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - using (HttpResponseMessage response = await client.GetAsync(WebApplicationExtendsTests.requestUri)) - { - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - await app.StopAsync(); - } - } - - /// Test - /// - /// - [TestMethod] - [DataRow("GET", HttpStatusCode.OK)] - [DataRow("POST", HttpStatusCode.MethodNotAllowed)] - public async Task TestUseHttpMethodChecker(string method, HttpStatusCode target) - { - const string methodAllowed = "GET"; - - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseHttpMethodChecker(methodAllowed); - app.MapGet(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - HttpRequestMessage msg = new( - new(method), - WebApplicationExtendsTests.requestUri - ); - using (HttpResponseMessage response = await client.SendAsync(msg)) - { - Assert.AreEqual(target, response.StatusCode); - } - await app.StopAsync(); - } - - } - - /// Test - /// - /// - [TestClass] - public class TestUseUnixTimestampChecker - { - /// Test - /// - /// When no timestamp in get request. - /// - [TestMethod] - public async Task TestUseUnixTimestampCheckerMissingGet() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - webApplicationBuilder.Services.AddSingleton(); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseUnixTimestampChecker(); - app.MapGet(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - using (HttpResponseMessage response = await client.GetAsync(WebApplicationExtendsTests.requestUri)) - { - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - await app.StopAsync(); - } - } - /// Test - /// - /// When no timestamp in post request. - /// - [TestMethod] - public async Task TestUseUnixTimestampCheckerMissingPost() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - webApplicationBuilder.Services.AddSingleton(); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseUnixTimestampChecker(); - app.MapPost(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - using (HttpResponseMessage response = await client.PostAsync( - WebApplicationExtendsTests.requestUri, - new StringContent("{}") - )) - { - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - await app.StopAsync(); - } - } - /// Test - /// - /// When invalid timestamp in get request. - /// - [TestMethod] - public async Task TestUseUnixTimestampCheckerInvalidGet() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer(); - webApplicationBuilder.Services.AddSingleton(); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseUnixTimestampChecker(); - app.MapGet(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - UnixTimestampGenerator timestampGenerator = new(); - long badTimestamp = timestampGenerator.GetUnixTimestamp() - 40 * 1000; - using (HttpResponseMessage response = await client.GetAsync( - string.Format( - "{0}?timestamp={1}", - WebApplicationExtendsTests.requestUri, - badTimestamp.ToString() - ) - )) - { - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - await app.StopAsync(); - } - } - /// Test - /// - /// When invalid timestamp in post request. - /// - [TestMethod] - public async Task TestUseUnixTimestampCheckerInvalidPost() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - webApplicationBuilder.Services.AddSingleton(); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseUnixTimestampChecker(); - app.MapPost(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - UnixTimestampGenerator timestampGenerator = new(); - long badTimestamp = timestampGenerator.GetUnixTimestamp() - 40 * 1000; - Dictionary data = new() - { - {"timestamp", badTimestamp.ToString()} - }; - using (HttpResponseMessage response = await client.PostAsync( - WebApplicationExtendsTests.requestUri, - JsonContent.Create(data) - )) - { - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - await app.StopAsync(); - } - } - /// Test - /// - /// When timestamp is correct in get request. - /// - [TestMethod] - public async Task TestUseUnixTimestampCheckerGet() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - webApplicationBuilder.Services.AddSingleton(); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseUnixTimestampChecker(); - app.MapGet(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - UnixTimestampGenerator timestampGenerator = new(); - long timestamp = timestampGenerator.GetUnixTimestamp(); - using (HttpResponseMessage response = await client.GetAsync( - string.Format( - "{0}?timestamp={1}", - WebApplicationExtendsTests.requestUri, - timestamp.ToString() - ) - )) - { - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - } - await app.StopAsync(); - } - } - /// Test - /// - /// When timestamp is correct in post request. - /// - [TestMethod] - public async Task TestUseUnixTimestampCheckerPost() - { - WebApplicationBuilder webApplicationBuilder = WebApplication.CreateBuilder(); - webApplicationBuilder.WebHost.UseTestServer((opt) => opt.BaseAddress = WebApplicationExtendsTests.baseAddress); - webApplicationBuilder.Services.AddSingleton(); - using (WebApplication app = webApplicationBuilder.Build()) - { - app.UseUnixTimestampChecker(); - app.MapPost(WebApplicationExtendsTests.requestUri, WebApplicationExtendsTests.responseAction); - await app.StartAsync(); - HttpClient client = app.GetTestClient(); - - UnixTimestampGenerator timestampGenerator = new(); - long timestamp = timestampGenerator.GetUnixTimestamp(); - Dictionary data = new() - { - {"timestamp", timestamp.ToString()} - }; - using (HttpResponseMessage response = await client.PostAsync( - WebApplicationExtendsTests.requestUri, - JsonContent.Create(data) - )) - { - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - } - await app.StopAsync(); - } - } - } -} diff --git a/E5Renewer.sln b/E5Renewer.sln index 173f7b8..f3933ed 100644 --- a/E5Renewer.sln +++ b/E5Renewer.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -7,14 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E5Renewer", "E5Renewer\E5Re EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E5Renewer.Tests", "E5Renewer.Tests\E5Renewer.Tests.csproj", "{74395711-5B07-4A82-841B-FCF2FF33D8D9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E5Renewer.Modules.TomlParser", "E5Renewer.Modules.TomlParser\E5Renewer.Modules.TomlParser.csproj", "{26DEFD21-4507-4FB3-89F7-D113C1157F7D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E5Renewer.Modules.YamlParser", "E5Renewer.Modules.YamlParser\E5Renewer.Modules.YamlParser.csproj", "{461922E6-887C-4C49-A703-EEAE9883A99E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E5Renewer.Modules.TomlParser.Tests", "E5Renewer.Modules.TomlParser.Tests\E5Renewer.Modules.TomlParser.Tests.csproj", "{79622D22-A66D-486D-B615-7917F6234F23}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E5Renewer.Modules.YamlParser.Tests", "E5Renewer.Modules.YamlParser.Tests\E5Renewer.Modules.YamlParser.Tests.csproj", "{088145AC-FF9F-4A20-8146-493587DF5D83}" -EndProject Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -52,53 +44,5 @@ Global {74395711-5B07-4A82-841B-FCF2FF33D8D9}.Release|x64.Build.0 = Release|Any CPU {74395711-5B07-4A82-841B-FCF2FF33D8D9}.Release|x86.ActiveCfg = Release|Any CPU {74395711-5B07-4A82-841B-FCF2FF33D8D9}.Release|x86.Build.0 = Release|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Debug|x64.ActiveCfg = Debug|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Debug|x64.Build.0 = Debug|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Debug|x86.ActiveCfg = Debug|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Debug|x86.Build.0 = Debug|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Release|Any CPU.Build.0 = Release|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Release|x64.ActiveCfg = Release|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Release|x64.Build.0 = Release|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Release|x86.ActiveCfg = Release|Any CPU - {26DEFD21-4507-4FB3-89F7-D113C1157F7D}.Release|x86.Build.0 = Release|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Debug|x64.ActiveCfg = Debug|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Debug|x64.Build.0 = Debug|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Debug|x86.ActiveCfg = Debug|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Debug|x86.Build.0 = Debug|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Release|Any CPU.Build.0 = Release|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Release|x64.ActiveCfg = Release|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Release|x64.Build.0 = Release|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Release|x86.ActiveCfg = Release|Any CPU - {461922E6-887C-4C49-A703-EEAE9883A99E}.Release|x86.Build.0 = Release|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Debug|x64.ActiveCfg = Debug|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Debug|x64.Build.0 = Debug|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Debug|x86.ActiveCfg = Debug|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Debug|x86.Build.0 = Debug|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Release|Any CPU.Build.0 = Release|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Release|x64.ActiveCfg = Release|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Release|x64.Build.0 = Release|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Release|x86.ActiveCfg = Release|Any CPU - {79622D22-A66D-486D-B615-7917F6234F23}.Release|x86.Build.0 = Release|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Debug|Any CPU.Build.0 = Debug|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Debug|x64.ActiveCfg = Debug|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Debug|x64.Build.0 = Debug|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Debug|x86.ActiveCfg = Debug|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Debug|x86.Build.0 = Debug|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Release|Any CPU.ActiveCfg = Release|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Release|Any CPU.Build.0 = Release|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Release|x64.ActiveCfg = Release|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Release|x64.Build.0 = Release|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Release|x86.ActiveCfg = Release|Any CPU - {088145AC-FF9F-4A20-8146-493587DF5D83}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/E5Renewer/AssemblyExtends.cs b/E5Renewer/AssemblyExtends.cs new file mode 100644 index 0000000..a4c1608 --- /dev/null +++ b/E5Renewer/AssemblyExtends.cs @@ -0,0 +1,16 @@ +using System.Reflection; + +using E5Renewer.Models.Modules; + +namespace E5Renewer +{ + internal static class AssemblyExtends + { + public static IEnumerable IterE5RenewerModules(this Assembly assembly) + { + return assembly.GetTypes().GetNonAbstractClassesAssainableTo().Where( + (t) => t.IsDefined(typeof(ModuleAttribute)) + ); + } + } +} diff --git a/E5Renewer/Controllers/IDummyResultGenerator.cs b/E5Renewer/Controllers/IDummyResultGenerator.cs new file mode 100644 index 0000000..f800e05 --- /dev/null +++ b/E5Renewer/Controllers/IDummyResultGenerator.cs @@ -0,0 +1,12 @@ +namespace E5Renewer.Controllers +{ + /// The api interface to generate dummy response. + public interface IDummyResultGenerator + { + /// Generate a dummy result when something not right. + public Task GenerateDummyResultAsync(HttpContext httpContext); + + /// Generate a dummy result when something not right. + public InvokeResult GenerateDummyResult(HttpContext httpContext); + } +} diff --git a/E5Renewer/Controllers/JsonAPIV1Controller.cs b/E5Renewer/Controllers/JsonAPIV1Controller.cs index 9fbb582..872492b 100644 --- a/E5Renewer/Controllers/JsonAPIV1Controller.cs +++ b/E5Renewer/Controllers/JsonAPIV1Controller.cs @@ -14,25 +14,24 @@ public class JsonAPIV1Controller : ControllerBase private readonly IStatusManager statusManager; private readonly IEnumerable apiFunctions; private readonly IUnixTimestampGenerator unixTimestampGenerator; + private readonly IDummyResultGenerator dummyResponseGenerator; /// Initialize controller by logger given. /// The logger to create logs. /// The implementation. /// The implementation. /// The implementation. + /// The implementation. /// All the params are injected by Asp.Net Core runtime. public JsonAPIV1Controller( ILogger logger, IStatusManager statusManager, IEnumerable apiFunctions, - IUnixTimestampGenerator unixTimestampGenerator - ) - { - this.logger = logger; - this.statusManager = statusManager; - this.apiFunctions = apiFunctions; - this.unixTimestampGenerator = unixTimestampGenerator; - } + IUnixTimestampGenerator unixTimestampGenerator, + IDummyResultGenerator dummyResponseGenerator + ) => + (this.logger, this.statusManager, this.apiFunctions, this.unixTimestampGenerator, this.dummyResponseGenerator) = + (logger, statusManager, apiFunctions, unixTimestampGenerator, dummyResponseGenerator); /// Handler for /v1/list_apis. [HttpGet("list_apis")] @@ -99,5 +98,10 @@ string apiName this.unixTimestampGenerator.GetUnixTimestamp() ); } + + /// Handler for /v1/*. + [Route("{*method}")] + public async ValueTask Handle() => + await this.dummyResponseGenerator.GenerateDummyResultAsync(this.HttpContext); } } diff --git a/E5Renewer/Controllers/SimpleDummyResultGenerator.cs b/E5Renewer/Controllers/SimpleDummyResultGenerator.cs new file mode 100644 index 0000000..882a73d --- /dev/null +++ b/E5Renewer/Controllers/SimpleDummyResultGenerator.cs @@ -0,0 +1,62 @@ +using System.Text.Json; + +using E5Renewer.Models.Statistics; + +namespace E5Renewer.Controllers +{ + /// Generate a dummy result when something is not right. + public class SimpleDummyResultGenerator : IDummyResultGenerator + { + private readonly ILogger logger; + private readonly IUnixTimestampGenerator unixTimestampGenerator; + + /// Initialize with arguments given. + /// The logger to generate log. + /// The implementation. + /// All parameters should be injected by Asp.Net Core. + public SimpleDummyResultGenerator(ILogger logger, IUnixTimestampGenerator unixTimestampGenerator) => + (this.logger, this.unixTimestampGenerator) = (logger, unixTimestampGenerator); + + /// + public async Task GenerateDummyResultAsync(HttpContext httpContext) + { + Dictionary queries; + switch (httpContext.Request.Method) + { + case "GET": + queries = httpContext.Request.Query.Select( + (kv) => new KeyValuePair(kv.Key, kv.Value.FirstOrDefault() as object) + ).ToDictionary(); + break; + case "POST": + byte[] buffer = new byte[httpContext.Request.ContentLength ?? httpContext.Request.Body.Length]; + int length = await httpContext.Request.Body.ReadAsync(buffer); + byte[] contents = buffer.Take(length).ToArray(); + queries = JsonSerializer.Deserialize>(contents) ?? new(); + break; + default: + queries = new(); + break; + } + if (queries.ContainsKey("timestamp")) + { + queries.Remove("timestamp"); + } + string fullPath = httpContext.Request.PathBase + httpContext.Request.Path; + int lastOfSlash = fullPath.LastIndexOf("/"); + int firstOfQuote = fullPath.IndexOf("?"); + string methodName = + firstOfQuote > lastOfSlash ? + fullPath.Substring(lastOfSlash + 1, firstOfQuote - lastOfSlash) : + fullPath.Substring(lastOfSlash + 1); + return new(methodName, + queries, + null, + this.unixTimestampGenerator.GetUnixTimestamp() + ); + } + + /// + public InvokeResult GenerateDummyResult(HttpContext httpContext) => this.GenerateDummyResultAsync(httpContext).Result; + } +} diff --git a/E5Renewer/Controllers/UnspecifiedController.cs b/E5Renewer/Controllers/UnspecifiedController.cs new file mode 100644 index 0000000..12777db --- /dev/null +++ b/E5Renewer/Controllers/UnspecifiedController.cs @@ -0,0 +1,31 @@ +using System.Net; + +using Microsoft.AspNetCore.Mvc; + +namespace E5Renewer.Controllers +{ + /// All unspecified requests controller. + [ApiController] + public class UnspecifiedController : ControllerBase + { + private readonly IDummyResultGenerator dummyResultGenerator; + private readonly ILogger logger; + + /// Initialize with parameters given. + /// The logger to generate log. + /// The implementation. + /// All parameters should be injected by Asp.Net Core. + public UnspecifiedController(ILogger logger, IDummyResultGenerator dummyResultGenerator) => + (this.logger, this.dummyResultGenerator) = (logger, dummyResultGenerator); + + /// Handle all unspecified requests. + [Route("{*method}")] + public async ValueTask Handle() + { + this.logger.LogWarning("Someone called a unspecified path {0}.", this.HttpContext.Request.Path); + this.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + return await this.dummyResultGenerator.GenerateDummyResultAsync(this.HttpContext); + } + + } +} diff --git a/E5Renewer/E5Renewer.csproj b/E5Renewer/E5Renewer.csproj index 9444cc7..51df3be 100644 --- a/E5Renewer/E5Renewer.csproj +++ b/E5Renewer/E5Renewer.csproj @@ -10,9 +10,10 @@ + - + diff --git a/E5Renewer/ILoggingBuilderExtends.cs b/E5Renewer/ILoggingBuilderExtends.cs new file mode 100644 index 0000000..10e8b49 --- /dev/null +++ b/E5Renewer/ILoggingBuilderExtends.cs @@ -0,0 +1,28 @@ +namespace E5Renewer +{ + internal static class ILoggingBuilderExtends + { + public static ILoggingBuilder AddConsole(this ILoggingBuilder builder, bool systemd, LogLevel level) + { + const string timeStampFormat = "yyyy-MM-dd HH:mm:ss "; + + builder.ClearProviders(); + if (systemd) + { + builder.AddSystemdConsole((config) => config.TimestampFormat = timeStampFormat); + } + else + { + builder.AddSimpleConsole( + (config) => + { + config.SingleLine = true; + config.TimestampFormat = timeStampFormat; + } + ); + } + builder.SetMinimumLevel(level); + return builder; + } + } +} diff --git a/E5Renewer/IServiceCollectionExtends.cs b/E5Renewer/IServiceCollectionExtends.cs new file mode 100644 index 0000000..ecce100 --- /dev/null +++ b/E5Renewer/IServiceCollectionExtends.cs @@ -0,0 +1,76 @@ +using System.Reflection; + +using E5Renewer.Controllers; +using E5Renewer.Models.BackgroundServices; +using E5Renewer.Models.GraphAPIs; +using E5Renewer.Models.Modules; +using E5Renewer.Models.Secrets; +using E5Renewer.Models.Statistics; + +namespace E5Renewer +{ + internal static class IServiceCollectionExtends + { + public static IServiceCollection AddDummyResultGenerator(this IServiceCollection services) => + services.AddTransient(); + + public static IServiceCollection AddAPIFunctionImplementations(this IServiceCollection services) + { + IEnumerable apiFunctionsTypes = Assembly.GetExecutingAssembly().GetTypes() + .GetNonAbstractClassesAssainableTo(); + foreach (Type t in apiFunctionsTypes) + { + services.AddSingleton(typeof(IAPIFunction), t); + } + return services; + } + + public static IServiceCollection AddHostedServices(this IServiceCollection services) + { + services.AddHostedService(); + return services; + } + + public static IServiceCollection AddSecretProvider(this IServiceCollection services) => + services.AddSingleton(); + + public static IServiceCollection AddStatusManager(this IServiceCollection services) => + services.AddSingleton(); + + public static IServiceCollection AddTimeStampGenerator(this IServiceCollection services) => + services.AddTransient(); + + public static IServiceCollection AddTokenOverride(this IServiceCollection services, string? token, FileInfo? tokenFile) => + services.AddSingleton(_ => new(token, tokenFile)); + + public static IServiceCollection AddUserSecretFile(this IServiceCollection services, FileInfo userSecret) => + services.AddKeyedSingleton(nameof(userSecret), userSecret); + + public static IServiceCollection AddModules(this IServiceCollection services, params Assembly[] assemblies) + { + foreach (Assembly assembly in assemblies) + { + foreach (Type t in assembly.IterE5RenewerModules()) + { + if (t.IsAssignableTo(typeof(IModulesChecker))) + { + services.AddSingleton(typeof(IModulesChecker), t); + } + else if (t.IsAssignableTo(typeof(IUserSecretLoader))) + { + services.AddSingleton(typeof(IUserSecretLoader), t); + } + else if (t.IsAssignableTo(typeof(IGraphAPICaller))) + { + services.AddSingleton(typeof(IGraphAPICaller), t); + } + else if (t.IsAssignableTo(typeof(IModule))) + { + services.AddSingleton(typeof(IModule), t); + } + } + } + return services; + } + } +} diff --git a/E5Renewer/Models/BackgroundServices/ModulesCheckerService.cs b/E5Renewer/Models/BackgroundServices/ModulesCheckerService.cs deleted file mode 100644 index 9a41336..0000000 --- a/E5Renewer/Models/BackgroundServices/ModulesCheckerService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using E5Renewer.Models.GraphAPIs; -using E5Renewer.Models.Modules; - -namespace E5Renewer.Models.BackgroundServices -{ - /// class to check modules passed to AspNet.Core part. - public class ModulesCheckerService : BackgroundService - { - private readonly IEnumerable graphAPICallers; - private readonly IEnumerable modulesCheckers; - private readonly IEnumerable aspNetModules; - - /// Initialize ModulesCheckerService with parameters given. - /// All GraphAPICallers modules. - /// All ModulesChecker modules. - /// All modules do not belongs to apicaller or modules checker. - /// All the parameters should be injected by Asp.Net Core. - public ModulesCheckerService(IEnumerable graphAPICallers, IEnumerable modulesCheckers, IEnumerable aspNetModules) - { - this.graphAPICallers = graphAPICallers; - this.modulesCheckers = modulesCheckers; - this.aspNetModules = aspNetModules; - } - - /// - protected override Task ExecuteAsync(CancellationToken token) - { - List modules = new List(); - modules.AddRange(this.graphAPICallers); - modules.AddRange(this.aspNetModules); - foreach (IModule module in modules) - { - foreach (IModulesChecker checker in this.modulesCheckers) - { - checker.CheckModules(module); - } - } - return Task.CompletedTask; - } - } -} diff --git a/E5Renewer/Models/BackgroundServices/PrepareUsersService.cs b/E5Renewer/Models/BackgroundServices/PrepareUsersService.cs index 66a1e7e..56d5ff8 100644 --- a/E5Renewer/Models/BackgroundServices/PrepareUsersService.cs +++ b/E5Renewer/Models/BackgroundServices/PrepareUsersService.cs @@ -1,5 +1,5 @@ -using E5Renewer.Models.Config; using E5Renewer.Models.GraphAPIs; +using E5Renewer.Models.Secrets; using E5Renewer.Models.Statistics; namespace E5Renewer.Models.BackgroundServices @@ -11,60 +11,64 @@ namespace E5Renewer.Models.BackgroundServices public class PrepareUsersService : BackgroundService { private readonly ILogger logger; - private readonly IEnumerable users; - private readonly IServiceProvider serviceProvider; + private readonly ISecretProvider secretProvider; private readonly IStatusManager statusManager; + private readonly IEnumerable apiCallers; + private readonly Dictionary callerCache = new(); /// Initialize PrepareUsersService with parameters. /// The logger to create log. - /// The GraphUsers to process. - /// The IServiceProvider implementation. - /// The IStatusManager implementation. + /// The implicit. + /// The implementation. + /// A series of implementations. /// All parameters should be injected by AspNet.Core. public PrepareUsersService( ILogger logger, - IEnumerable users, - IServiceProvider serviceProvider, - IStatusManager statusManager + ISecretProvider secretProvider, + IStatusManager statusManager, + IEnumerable apiCallers ) { this.logger = logger; - this.users = users; - this.serviceProvider = serviceProvider; + this.secretProvider = secretProvider; this.statusManager = statusManager; + this.apiCallers = apiCallers; } /// protected override async Task ExecuteAsync(CancellationToken token) { - foreach (GraphUser user in this.users) + IEnumerable users = + (await this.secretProvider.GetUserSecretAsync()).users; + // TODO: Parallel? + foreach (User user in users) { - await this.DoAPICallForUser(token, user, this.statusManager); - } - - } - - private async Task DoAPICallForUser(CancellationToken token, GraphUser user, IStatusManager statusManager) - { - IGraphAPICaller apiCaller = this.serviceProvider.GetRequiredKeyedService(user); - while (!token.IsCancellationRequested) - { - await statusManager.SetUserStatusAsync(user.name, user.enabled); - if (user.enabled) - { - await apiCaller.CallNextAPIAsync(user); - await apiCaller.CalmDownAsync(token, user); - } - else + while (!token.IsCancellationRequested && user.valid) { - TimeSpan delay = user.timeToStart; - this.logger.LogDebug( - "Sleeping for {0} day(s), {1} hour(s), {2} miniute(s), {3} second(s) and {4} millisecond(s) to wait starting user {5}...", - delay.Days, delay.Hours, delay.Minutes, delay.Seconds, delay.Milliseconds, user.name - ); - await Task.Delay(delay, token); + bool enabled = user.timeToStart == TimeSpan.Zero; + await this.statusManager.SetUserStatusAsync(user.name, enabled); + if (enabled) + { + if (!this.callerCache.ContainsKey(user)) + { + Random random = new(); + this.callerCache[user] = random.GetItems(this.apiCallers.ToArray(), 1)[0]; + } + await this.callerCache[user].CallNextAPIAsync(user); + await this.callerCache[user].CalmDownAsync(token, user); + } + else + { + TimeSpan delay = user.timeToStart; + this.logger.LogDebug( + "Sleeping for {0} day(s), {1} hour(s), {2} miniute(s), {3} second(s) and {4} millisecond(s) to wait starting user {5}...", + delay.Days, delay.Hours, delay.Minutes, delay.Seconds, delay.Milliseconds, user.name + ); + await Task.Delay(delay, token); + } } } + } } } diff --git a/E5Renewer/Models/Config/Config.cs b/E5Renewer/Models/Config/Config.cs deleted file mode 100644 index bafce6c..0000000 --- a/E5Renewer/Models/Config/Config.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace E5Renewer.Models.Config -{ - /// class to store program config. - /// For compatibility to Python version. - public sealed record class Config : ICheckable - { - /// Authentication token. - public string authToken { get; set; } - - /// HTTP json api listen address. - public string listenAddr { get; set; } - - /// HTTP json api listen port. - public uint listenPort { get; set; } - - ///
- /// Use Unix Domain Socket instead TCP for HTTP json api.
- ///
- /// - /// Only available when HTTP listen failed and your plaform supports Unix Domain Socket. - /// - public string listenSocket { get; set; } - - ///
- /// The permission of Unix Domain Socket.
- ///
- /// - /// In octal number like 777 or 644. - /// - public uint listenSocketPermission { get; set; } - - /// The list of - /// GraphUsers - /// to access msgraph apis. - public List users { get; set; } - - /// The map of passwords for certificate. - public Dictionary? passwords { get; set; } - - /// - public bool isCheckPassed - { - get - { - return !string.IsNullOrEmpty(this.authToken) && - users.All( - (user) => user.isCheckPassed - ); - } - } - - /// Initialize Config with default values. - public Config() - { - this.authToken = ""; - this.listenAddr = "127.0.0.1"; - this.listenPort = 8888; - this.listenSocket = "/run/e5renewer/e5renewer.socket"; - this.listenSocketPermission = Convert.ToUInt32("666", 8); - this.users = new(); - } - } -} diff --git a/E5Renewer/Models/Config/ConfigCertificatePasswordProvider.cs b/E5Renewer/Models/Config/ConfigCertificatePasswordProvider.cs deleted file mode 100644 index 8b8d5f8..0000000 --- a/E5Renewer/Models/Config/ConfigCertificatePasswordProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Security.Cryptography; - -namespace E5Renewer.Models.Config -{ - /// Get password for a certificate from config. - public class ConfigCertificatePasswordProvider : ICertificatePasswordProvider - { - private readonly ILogger logger; - private readonly Dictionary? passwords; - private readonly Dictionary cache = new(); - - /// Initialize ConfigCertificatePasswordProvider with parameters given. - /// The logger to create log. - /// The passwords of certificates. Defaults to null - /// All parameters should be injected by Asp.Net Core. - public ConfigCertificatePasswordProvider( - ILogger logger, - Dictionary? passwords = null - ) - { - this.passwords = passwords; - this.logger = logger; - } - - /// - public async Task GetPasswordForCertificateAsync(string certificate) - { - if (!this.cache.ContainsKey(certificate)) - { - if (File.Exists(certificate)) - { - byte[] sha512Hash; - using (Stream stream = File.OpenRead(certificate)) - { - sha512Hash = await SHA512.HashDataAsync(stream); - } - string fileHash = BitConverter.ToString(sha512Hash).Replace("-", "").ToLower(); - this.logger.LogDebug("File hash for {0} is {1}", certificate, fileHash); - this.cache[certificate] = - this.passwords is Dictionary passwordsDict && passwordsDict.ContainsKey(fileHash) - ? passwordsDict[fileHash] - : null - ; - } - } - return this.cache[certificate]; - } - } -} diff --git a/E5Renewer/Models/Config/GraphUser.cs b/E5Renewer/Models/Config/GraphUser.cs deleted file mode 100644 index 4f901b8..0000000 --- a/E5Renewer/Models/Config/GraphUser.cs +++ /dev/null @@ -1,139 +0,0 @@ -namespace E5Renewer.Models.Config -{ - /// class to store info to access msgraph apis. - /// For compatibility to Python version. - public sealed record class GraphUser : ICheckable - { - /// The name of user. - public string name { get; set; } - - /// The tenant id of user. - public string tenantId { get; set; } - - /// The client id of user. - public string clientId { get; set; } - - /// The secret of user. - public string secret { get; set; } - - /// The path to certificate of user. - public string certificate { get; set; } - - /// When to start the user. - public TimeOnly fromTime { get; set; } - - /// When to stop the user. - public TimeOnly toTime { get; set; } - - /// Which days are allowed to start the user. - public List days { get; set; } - - /// When to start the user. - public bool isCheckPassed - { - get - { - string[] items = [this.name, this.tenantId, this.clientId]; - return items.All((item) => !string.IsNullOrEmpty(item)) && this.IsCertificateOrSecretValid(); - } - } - - /// If the user is allowed to start. - public bool enabled - { - get - { - DateTime now = DateTime.Now; - DateOnly today = DateOnly.FromDateTime(now); - DateTime fromTime = new(today, this.fromTime); - DateTime toTime = new(today, this.toTime); - while (fromTime >= toTime) - { - toTime = toTime.AddDays(1); - } - return now >= fromTime && - now < toTime && - this.days.Contains(now.DayOfWeek); - } - } - - /// How long to start the user. - public TimeSpan timeToStart - { - get - { - if (this.enabled) - { - return new(); - } - DateTime now = DateTime.Now; - DateOnly today = DateOnly.FromDateTime(now); - DateTime fromTime = new(today, this.fromTime); - DateTime toTime = new(today, this.toTime); - while (fromTime >= toTime) - { - toTime = toTime.AddDays(1); - } - if (now < fromTime) - { - return fromTime - now; - } - if (now >= toTime) - { - return this.GetSuitableDateTime( - now, - (item) => now < item, - (date) => - new( - DateOnly.FromDateTime(date).AddDays(1), - this.fromTime - ) - ) - now; - } - // this.days do not contains today - return this.GetSuitableDateTime( - now, - (item) => this.days.Contains(item.DayOfWeek), - (date) => - new( - DateOnly.FromDateTime(date).AddDays(1), - this.fromTime - ) - ) - now; - } - } - - /// - /// Initialize GraphUser with default values. - /// - public GraphUser() - { - this.name = ""; - this.tenantId = ""; - this.clientId = ""; - this.secret = ""; - this.certificate = ""; - this.fromTime = new(8, 0); - this.toTime = new(16, 0); - this.days = new() - { - DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, - DayOfWeek.Thursday, DayOfWeek.Friday - }; - } - - private DateTime GetSuitableDateTime(DateTime start, Predicate passCondition, Func genNextItem) - { - while (!passCondition(start)) - { - start = genNextItem(start); - } - return start; - } - - private bool IsCertificateOrSecretValid() - { - return File.Exists(this.certificate) || !string.IsNullOrEmpty(this.secret); - } - } -} diff --git a/E5Renewer/Models/Config/ICertificatePasswordProvider.cs b/E5Renewer/Models/Config/ICertificatePasswordProvider.cs deleted file mode 100644 index e088ba8..0000000 --- a/E5Renewer/Models/Config/ICertificatePasswordProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace E5Renewer.Models.Config -{ - /// The api interface for getting password for a certificate. - public interface ICertificatePasswordProvider - { - /// Get password for a certificate. - /// The password for the certificate given. null if no password. - public Task GetPasswordForCertificateAsync(string certificate); - } -} diff --git a/E5Renewer/Models/Config/ICheckable.cs b/E5Renewer/Models/Config/ICheckable.cs deleted file mode 100644 index 1f32c29..0000000 --- a/E5Renewer/Models/Config/ICheckable.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace E5Renewer.Models.Config -{ - /// The api interface for checkable object. - public interface ICheckable - { - /// If the object is checked. - public bool isCheckPassed { get; } - } -} diff --git a/E5Renewer/Models/Config/IConfigParser.cs b/E5Renewer/Models/Config/IConfigParser.cs deleted file mode 100644 index 35981cf..0000000 --- a/E5Renewer/Models/Config/IConfigParser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using E5Renewer.Models.Modules; - -namespace E5Renewer.Models.Config -{ - /// The api interface for config parser. - public interface IConfigParser : IModule - { - /// If this parser supports path given. - /// The path to the config. - public bool IsSupported(string path); - - /// If this parser supports path given. - /// The FileInfo to the config. - public bool IsSupported(FileInfo fileInfo) - { - return IsSupported(fileInfo.FullName); - } - - /// Parse config. - /// The path to the config. - /// Parsed result. - public ValueTask ParseConfigAsync(string path); - - /// Parse config. - /// The FileInfo to the config. - /// Parsed result. - public async ValueTask ParseConfigAsync(FileInfo fileInfo) - { - return await this.ParseConfigAsync(fileInfo.FullName); - } - } -} diff --git a/E5Renewer/Models/Config/JsonConfigParser.cs b/E5Renewer/Models/Config/JsonConfigParser.cs deleted file mode 100644 index 4d1a11d..0000000 --- a/E5Renewer/Models/Config/JsonConfigParser.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Text.Json; - -using E5Renewer.Models.Modules; - -namespace E5Renewer.Models.Config -{ - /// class for parsing json config. - [Module] - public class JsonConfigParser : IConfigParser - { - /// - public string name { get => nameof(JsonConfigParser); } - - /// - public string author { get => "E5Renewer"; } - - /// - public SemanticVersioning.Version apiVersion - { - get => typeof(JsonConfigParser).Assembly.GetName().Version?.ToSemanticVersion() ?? new(0, 1, 0); - } - - /// - public bool IsSupported(string path) => path.EndsWith(".json"); - - /// - public async ValueTask ParseConfigAsync(string path) - { - JsonSerializerOptions options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower - }; - using (FileStream fileStream = File.OpenRead(path)) - { - return await JsonSerializer.DeserializeAsync(fileStream, options) ?? new(); - } - } - } -} diff --git a/E5Renewer/Models/GraphAPIs/IGraphAPICaller.cs b/E5Renewer/Models/GraphAPIs/IGraphAPICaller.cs index 687ac8c..69637ce 100644 --- a/E5Renewer/Models/GraphAPIs/IGraphAPICaller.cs +++ b/E5Renewer/Models/GraphAPIs/IGraphAPICaller.cs @@ -1,18 +1,18 @@ -using E5Renewer.Models.Config; using E5Renewer.Models.Modules; +using E5Renewer.Models.Secrets; namespace E5Renewer.Models.GraphAPIs { /// The api interface of msgraph apis callers. - public interface IGraphAPICaller : IAspNetModule + public interface IGraphAPICaller : IModule { /// Call next api for user. /// The user to call api. - public Task CallNextAPIAsync(GraphUser user); + public Task CallNextAPIAsync(User user); /// Wait for some time after one msgraph api is called. /// The token to cancel calming down. /// The user to calm down. - public Task CalmDownAsync(CancellationToken token, GraphUser user); + public Task CalmDownAsync(CancellationToken token, User user); } } diff --git a/E5Renewer/Models/GraphAPIs/RandomGraphAPICaller.cs b/E5Renewer/Models/GraphAPIs/RandomGraphAPICaller.cs index fbecbd2..f742357 100644 --- a/E5Renewer/Models/GraphAPIs/RandomGraphAPICaller.cs +++ b/E5Renewer/Models/GraphAPIs/RandomGraphAPICaller.cs @@ -3,8 +3,8 @@ using Azure.Core; using Azure.Identity; -using E5Renewer.Models.Config; using E5Renewer.Models.Modules; +using E5Renewer.Models.Secrets; using E5Renewer.Models.Statistics; using Microsoft.Graph; @@ -21,26 +21,26 @@ public class RandomGraphAPICaller : IGraphAPICaller private readonly ILogger logger; private readonly IEnumerable apiFunctions; private readonly IStatusManager statusManager; - private readonly IEnumerable certificatePasswordProviders; - private readonly Dictionary clients = new(); + private readonly ISecretProvider secretProvider; + private readonly Dictionary clients = new(); /// Initialize RandomGraphAPICaller with parameters given. /// The logger to generate log. /// All known api functions with their id. - /// IStatusManager implementation. - /// ICertificatePasswordProvider implementations. + /// implementation. + /// implementations. /// All parameters should be injected by Asp.Net Core. public RandomGraphAPICaller( ILogger logger, IEnumerable apiFunctions, IStatusManager statusManager, - IEnumerable certificatePasswordProviders + ISecretProvider secretProvider ) { this.logger = logger; this.apiFunctions = apiFunctions; this.statusManager = statusManager; - this.certificatePasswordProviders = certificatePasswordProviders; + this.secretProvider = secretProvider; this.logger.LogDebug("Found {0} api functions", this.apiFunctions.Count()); } @@ -57,7 +57,7 @@ public SemanticVersioning.Version apiVersion } /// - public async Task CallNextAPIAsync(GraphUser user) + public async Task CallNextAPIAsync(User user) { if (!this.clients.ContainsKey(user)) { @@ -66,22 +66,31 @@ public async Task CallNextAPIAsync(GraphUser user) AuthorityHost = AzureAuthorityHosts.AzurePublicCloud }; TokenCredential credential; - if (File.Exists(user.certificate)) + if (user.certificate?.Exists ?? false) { this.logger.LogDebug("Using certificate to get user token."); - string? password = await this.GetPasswordForCertificateAsync(user.certificate); + string? password = await this.secretProvider.GetPasswordForCertificateAsync(user.certificate); if (password is not null) { this.logger.LogDebug("Found password for certificate given."); } - X509Certificate2 certificate = new(user.certificate, password); - credential = new ClientCertificateCredential(user.tenantId, user.clientId, certificate, options); + using (FileStream fileStream = user.certificate.OpenRead()) + { + byte[] buffer = new byte[user.certificate.Length]; + int size = await fileStream.ReadAsync(buffer); + X509Certificate2 certificate = new(buffer.Take(size).ToArray(), password); + credential = new ClientCertificateCredential(user.tenantId, user.clientId, certificate, options); + } } - else + else if (!string.IsNullOrEmpty(user.secret)) { this.logger.LogDebug("Using secret to get user token."); credential = new ClientSecretCredential(user.tenantId, user.clientId, user.secret, options); } + else + { + throw new NullReferenceException($"{nameof(user.certificate)} and {nameof(user.secret)} are both invalid."); + } GraphServiceClient client = new(credential, ["https://graph.microsoft.com/.default"]); this.clients[user] = client; } @@ -107,9 +116,9 @@ int GetFunctionWeightOfCurrentUser(IAPIFunction function) } /// - public async Task CalmDownAsync(CancellationToken token, GraphUser user) + public async Task CalmDownAsync(CancellationToken token, User user) { - if (user.enabled) + if (user.timeToStart == TimeSpan.Zero) { const int calmDownMinMilliSeconds = 300000; const int calmDownMaxMilliSeconds = 500000; @@ -119,18 +128,5 @@ public async Task CalmDownAsync(CancellationToken token, GraphUser user) await Task.Delay(milliseconds, token); } } - - private async Task GetPasswordForCertificateAsync(string certificate) - { - foreach (ICertificatePasswordProvider provider in this.certificatePasswordProviders) - { - string? password = await provider.GetPasswordForCertificateAsync(certificate); - if (password is not null) - { - return password; - } - } - return null; - } } } diff --git a/E5Renewer/Models/Modules/DeprecatedModulesChecker.cs b/E5Renewer/Models/Modules/DeprecatedModulesChecker.cs index bd6cc38..9585de3 100644 --- a/E5Renewer/Models/Modules/DeprecatedModulesChecker.cs +++ b/E5Renewer/Models/Modules/DeprecatedModulesChecker.cs @@ -7,12 +7,10 @@ public class DeprecatedModulesChecker : IModulesChecker private readonly ILogger logger; /// Initialize the DeprecatedModulesChecker instance. - /// The logger factory to create logger to generate outputs. + /// The logger factory to create logger to generate outputs. /// All the parameters should be injected by Asp.Net Core. - public DeprecatedModulesChecker(ILoggerFactory loggerFactory) - { - this.logger = loggerFactory.CreateLogger(); - } + public DeprecatedModulesChecker(ILogger logger) => + this.logger = logger; /// public string name { get => nameof(DeprecatedModulesChecker); } diff --git a/E5Renewer/Models/Modules/IAspNetModule.cs b/E5Renewer/Models/Modules/IAspNetModule.cs deleted file mode 100644 index 64d10aa..0000000 --- a/E5Renewer/Models/Modules/IAspNetModule.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace E5Renewer.Models.Modules -{ - /// The api interface for modules should be injected into AspNet. - public interface IAspNetModule : IModule { } -} diff --git a/E5Renewer/Models/Modules/IModulesChecker.cs b/E5Renewer/Models/Modules/IModulesChecker.cs index 9a3aede..1176580 100644 --- a/E5Renewer/Models/Modules/IModulesChecker.cs +++ b/E5Renewer/Models/Modules/IModulesChecker.cs @@ -1,7 +1,7 @@ namespace E5Renewer.Models.Modules { /// The api interface of checking module. - public interface IModulesChecker : IAspNetModule + public interface IModulesChecker : IModule { /// Check the module. /// The module to check. diff --git a/E5Renewer/Models/Secrets/ISecretProvider.cs b/E5Renewer/Models/Secrets/ISecretProvider.cs new file mode 100644 index 0000000..0807c37 --- /dev/null +++ b/E5Renewer/Models/Secrets/ISecretProvider.cs @@ -0,0 +1,19 @@ +namespace E5Renewer.Models.Secrets +{ + /// The api interface for getting secrets. + public interface ISecretProvider + { + /// Get password for certificate. + /// The password of the certificate, null if not found. + public Task GetPasswordForCertificateAsync(FileInfo certificate); + + /// Generate runtime token for authentication. + /// The token. + public Task GetRuntimeTokenAsync(); + + /// Get user secrets from file. + /// A series of loaded from file. + /// Thorw if no loader can be found to load secret. + public Task GetUserSecretAsync(); + } +} diff --git a/E5Renewer/Models/Secrets/IUserSecretLoader.cs b/E5Renewer/Models/Secrets/IUserSecretLoader.cs new file mode 100644 index 0000000..be0d2b5 --- /dev/null +++ b/E5Renewer/Models/Secrets/IUserSecretLoader.cs @@ -0,0 +1,14 @@ +using E5Renewer.Models.Modules; + +namespace E5Renewer.Models.Secrets +{ + /// The api interface for loading user secret. + public interface IUserSecretLoader : IModule + { + /// If file given is supported by the loader. + public bool IsSupported(FileInfo secretFile); + + /// Load a series of from file given. + public Task LoadSecretAsync(FileInfo secretFile); + } +} diff --git a/E5Renewer/Models/Secrets/Json/FileInfoOrNullConverter.cs b/E5Renewer/Models/Secrets/Json/FileInfoOrNullConverter.cs new file mode 100644 index 0000000..eb26e0a --- /dev/null +++ b/E5Renewer/Models/Secrets/Json/FileInfoOrNullConverter.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace E5Renewer.Models.Secrets.Json +{ + /// Custom to convert between and . + internal class FileInfoOrNullConverter : JsonConverter + { + /// + public override FileInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string? read = reader.GetString(); + return read is not null ? new(read) : null; + } + + /// + public override void Write(Utf8JsonWriter writer, FileInfo? value, JsonSerializerOptions options) => writer.WriteStringValue(value?.FullName); + + } +} diff --git a/E5Renewer/Models/Secrets/Json/JsonUserSecretLoader.cs b/E5Renewer/Models/Secrets/Json/JsonUserSecretLoader.cs new file mode 100644 index 0000000..94d2883 --- /dev/null +++ b/E5Renewer/Models/Secrets/Json/JsonUserSecretLoader.cs @@ -0,0 +1,51 @@ +using System.Text.Json; + +using E5Renewer.Models.Modules; + +namespace E5Renewer.Models.Secrets.Json +{ + /// Class for loading user secret from json file. + [Module] + public class JsonUserSecretLoader : IUserSecretLoader + { + private readonly Dictionary cache = new(); + + /// + public string name { get => nameof(JsonUserSecretLoader); } + + /// + public string author { get => "E5Renewer"; } + + /// + public SemanticVersioning.Version apiVersion + { + get => typeof(JsonUserSecretLoader).Assembly.GetName().Version?.ToSemanticVersion() ?? new(0, 1, 0); + } + + /// + public bool IsSupported(FileInfo userSecret) + { + return userSecret.Name.EndsWith(".json"); + } + + /// + public async Task LoadSecretAsync(FileInfo userSecret) + { + if (!this.cache.ContainsKey(userSecret)) + { + JsonSerializerOptions options = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + IncludeFields = true + }; + options.Converters.Add(new FileInfoOrNullConverter()); + using (FileStream fileStream = userSecret.OpenRead()) + { + this.cache[userSecret] = await JsonSerializer.DeserializeAsync(fileStream, options); + } + } + return this.cache[userSecret]; + + } + } +} diff --git a/E5Renewer/Models/Secrets/SimpleSecretProvider.cs b/E5Renewer/Models/Secrets/SimpleSecretProvider.cs new file mode 100644 index 0000000..bfd4aaf --- /dev/null +++ b/E5Renewer/Models/Secrets/SimpleSecretProvider.cs @@ -0,0 +1,133 @@ +using System.Collections.Immutable; +using System.Security.Cryptography; + +namespace E5Renewer.Models.Secrets +{ + /// Simple implementation of . + public class SimpleSecretProvider : ISecretProvider + { + private readonly FileInfo userSecret; + private readonly ILogger logger; + private readonly IEnumerable secretLoaders; + private readonly TokenOverride? tokenOverride; + private readonly string randomGeneratedToken; + private readonly Dictionary certificatePasswordsCache = new(); + private bool notifyRandom = true; + private UserSecret? secretCache = null; + + /// Initialize with parameters given. + /// The logger to create logs. + /// The file contains user secret info. + /// The implementation. + /// The instance. + /// All parameters should be injected by Asp.Net Core. + public SimpleSecretProvider( + ILogger logger, + [FromKeyedServices(nameof(userSecret))] FileInfo userSecret, + IEnumerable secretLoaders, + TokenOverride? tokenOverride = null + ) + { + this.logger = logger; + this.userSecret = userSecret; + this.secretLoaders = secretLoaders; + this.tokenOverride = tokenOverride; + + const int tokenSize = 32; + const string tokenSource = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYZ0123456789"; + Random random = new(); + this.randomGeneratedToken = ""; + for (int i = 0; i < tokenSize; i++) + { + this.randomGeneratedToken += tokenSource[random.Next(tokenSource.Length)]; + } + } + + /// + public async Task GetPasswordForCertificateAsync(FileInfo certificate) + { + if (this.certificatePasswordsCache.ContainsKey(certificate)) + { + return this.certificatePasswordsCache[certificate]; + } + else if (certificate.Exists) + { + ImmutableDictionary passwords = + (await this.GetUserSecretAsync()).passwords ?? ImmutableDictionary.Create(); + byte[] sha512Hash; + using (Stream stream = certificate.OpenRead()) + { + sha512Hash = await SHA512.HashDataAsync(stream); + } + string fileHash = BitConverter.ToString(sha512Hash).Replace("-", "").ToLower(); + this.logger.LogDebug("File hash for {0} is {1}", certificate, fileHash); + this.certificatePasswordsCache[certificate] = + passwords.ContainsKey(fileHash) + ? passwords[fileHash] + : null + ; + return this.certificatePasswordsCache[certificate]; + } + else + { + return null; + } + } + + /// + public async Task GetRuntimeTokenAsync() + { + string? token = this.tokenOverride?.token; + FileInfo? tokenFile = this.tokenOverride?.tokenFile; + + if (token is not null) + { + return token; + } + + if (tokenFile is not null) + { + using (StreamReader stream = tokenFile.OpenText()) + { + string? line = await stream.ReadLineAsync(); + if (line is not null) + { + return line.Trim(); + } + } + } + if (this.notifyRandom) + { + this.logger.LogWarning("Using random generated token {0}", this.randomGeneratedToken); + this.notifyRandom = false; + } + return this.randomGeneratedToken; + } + + /// + public async Task GetUserSecretAsync() + { + if (this.secretCache is UserSecret secretCache) + { + return secretCache; + } + try + { + IUserSecretLoader loader = this.secretLoaders.First((l) => l.IsSupported(this.userSecret)); + this.logger.LogDebug("Using {0} to load user secret {1}.", loader.name, this.userSecret); + UserSecret secretResult = await loader.LoadSecretAsync(this.userSecret); + this.secretCache = secretResult; + return secretResult; + } + catch (InvalidOperationException ioe) + { + this.logger.LogError("Failed to find {0} for user secret from because {1}.", nameof(IUserSecretLoader), ioe.Message); + } + catch (Exception e) + { + this.logger.LogError("Failed to load user secret from because {0}.", e.Message); + } + throw new InvalidDataException($"Cannot load {this.userSecret}."); + } + } +} diff --git a/E5Renewer/Models/Secrets/TokenOverride.cs b/E5Renewer/Models/Secrets/TokenOverride.cs new file mode 100644 index 0000000..5a234b2 --- /dev/null +++ b/E5Renewer/Models/Secrets/TokenOverride.cs @@ -0,0 +1,19 @@ +namespace E5Renewer.Models.Secrets +{ + /// Token overrides random generated one. + public class TokenOverride + { + /// Override in plain text. + public readonly string? token; + + /// Override in file content. + public readonly FileInfo? tokenFile; + + /// Initialize with arguments given. + public TokenOverride(string? token, FileInfo? tokenFile) + { + this.token = token; + this.tokenFile = tokenFile; + } + } +} diff --git a/E5Renewer/Models/Secrets/Toml/TomlDocumentNodeExtends.cs b/E5Renewer/Models/Secrets/Toml/TomlDocumentNodeExtends.cs new file mode 100644 index 0000000..9bafb96 --- /dev/null +++ b/E5Renewer/Models/Secrets/Toml/TomlDocumentNodeExtends.cs @@ -0,0 +1,56 @@ +using System.Collections.Immutable; + +using CaseConverter; + +using CsToml; + +namespace E5Renewer.Models.Secrets.Toml +{ + internal static class TomlDocumentNodeExtends + { + public static UserSecret ToUserSecret(this TomlDocumentNode node) + { + List users = new(); + TomlDocumentNode usersNode = node[nameof(users).ToSnakeCase()]; + for (int i = 0; i < usersNode.GetArray().Count; i++) + { + users.Add(usersNode[i].ToUser()); + } + + Dictionary? passwords; + bool passwordsDeserialized = node[nameof(passwords).ToSnakeCase()].TryGetValue>(out passwords); + + return new( + ImmutableList.CreateRange(users), + passwordsDeserialized && passwords is not null ? ImmutableDictionary.CreateRange(passwords) : null + ); + } + + public static User ToUser(this TomlDocumentNode node) + { + string name, tenantId, clientId; + name = node[nameof(name).ToSnakeCase()].GetString(); + tenantId = node[nameof(tenantId).ToSnakeCase()].GetString(); + clientId = node[nameof(clientId).ToSnakeCase()].GetString(); + + string? secret; + secret = node[nameof(secret).ToSnakeCase()].TryGetString(out secret) ? secret : null; + + FileInfo? certificate; + certificate = node[nameof(certificate).ToSnakeCase()].TryGetString(out string path) ? new(path) : null; + + TimeOnly? fromTime, toTime; + fromTime = node[nameof(fromTime).ToSnakeCase()].TryGetTimeOnly(out TimeOnly resultFromTime) ? resultFromTime : null; + toTime = node[nameof(toTime).ToSnakeCase()].TryGetTimeOnly(out TimeOnly resultToTime) ? resultToTime : null; + + List? days; + bool daysDeserialized = node[nameof(days).ToSnakeCase()].TryGetValue>(out days); + + return new( + name, tenantId, clientId, secret, certificate, + fromTime, toTime, + daysDeserialized && days is not null ? ImmutableList.CreateRange(days) : null + ); + } + } +} diff --git a/E5Renewer/Models/Secrets/Toml/TomlUserSecretLoader.cs b/E5Renewer/Models/Secrets/Toml/TomlUserSecretLoader.cs new file mode 100644 index 0000000..2d917f6 --- /dev/null +++ b/E5Renewer/Models/Secrets/Toml/TomlUserSecretLoader.cs @@ -0,0 +1,40 @@ +using CsToml; + +using E5Renewer.Models.Modules; + +namespace E5Renewer.Models.Secrets.Toml +{ + /// Class for loading user secret from toml file. + [Module] + public class TomlUserSecretLoader : IUserSecretLoader + { + /// + public string name { get => nameof(TomlUserSecretLoader); } + + /// + public string author { get => "E5Renewer"; } + + /// + public SemanticVersioning.Version apiVersion + { + get => typeof(TomlUserSecretLoader).Assembly.GetName().Version?.ToSemanticVersion() ?? new(0, 1, 0); + } + + /// + public bool IsSupported(FileInfo userSecret) => userSecret.Name.EndsWith(".toml"); + + /// + public async Task LoadSecretAsync(FileInfo userSecret) + { + TomlDocument document; + using (FileStream fileStream = userSecret.OpenRead()) + { + byte[] buffer = new byte[userSecret.Length]; + int length = await fileStream.ReadAsync(buffer); + document = CsTomlSerializer.Deserialize(buffer.Take(length).ToArray()); + } + + return document.RootNode.ToUserSecret(); + } + } +} diff --git a/E5Renewer/Models/Secrets/UserSecret.cs b/E5Renewer/Models/Secrets/UserSecret.cs new file mode 100644 index 0000000..26b7c85 --- /dev/null +++ b/E5Renewer/Models/Secrets/UserSecret.cs @@ -0,0 +1,119 @@ +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +namespace E5Renewer.Models.Secrets +{ + /// Readonly struct to store user secret and certificate passwords. + public readonly struct UserSecret + { + /// Users in the secret. + public ImmutableList users { get; } + + /// Passwords for user certificates in the secret. + public ImmutableDictionary? passwords { get; } + + /// If this is correct to be used. + [JsonIgnore] + public bool valid { get => this.users.All((user) => user.valid); } + + /// Initialize instance with arguments given. + [JsonConstructor] + public UserSecret(ImmutableList users, ImmutableDictionary? passwords) => + (this.users, this.passwords) = (users, passwords); + } + + /// Readonly struct to store user secret. + public readonly struct User + { + /// The name of user. + public string name { get; } + + /// The tenant id of user. + public string tenantId { get; } + + /// The client id of user. + public string clientId { get; } + + /// The secret of user. + public string? secret { get; } + + /// The path to certificate of user. + public FileInfo? certificate { get; } + + /// When to start the user. + [JsonInclude] + private TimeOnly? fromTime { get; } + + /// When to stop the user. + [JsonInclude] + private TimeOnly? toTime { get; } + + /// Which days are allowed to start the user. + [JsonInclude] + private ImmutableList? days { get; } + + /// If this secret is valid to be used. + [JsonIgnore] + public bool valid + { + get + { + string[] nonEmptyItems = [this.name, this.tenantId, this.clientId]; + return nonEmptyItems.All((i) => !string.IsNullOrEmpty(i)) && + (this.certificate?.Exists ?? false || !string.IsNullOrEmpty(this.secret)); + } + } + + /// How long to start the user. + [JsonIgnore] + public TimeSpan timeToStart + { + get + { + TimeOnly fromTime = this.fromTime ?? new(8, 0); + TimeOnly toTime = this.toTime ?? new(16, 0); + ImmutableList days = this.days ?? ImmutableList.Create( + DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday); + + DateTime now = DateTime.Now; + DateOnly today = DateOnly.FromDateTime(now); + DateTime fromDateTime = new(today, fromTime); + DateTime toDateTime = new(today, toTime); + while (fromDateTime >= toDateTime) + { + toDateTime = toDateTime.AddDays(1); + } + if (now <= fromDateTime) + { + DateTime nextFromTime = fromDateTime; + while (!days.Contains(nextFromTime.DayOfWeek)) + { + nextFromTime = nextFromTime.AddDays(1); + } + return nextFromTime - now; + } + else + { + DateTime nextFromDateTime = now; + DateTime nextToDateTime = toDateTime; + while (!days.Contains(nextFromDateTime.DayOfWeek) || nextFromDateTime >= nextToDateTime) + { + DateOnly startDate = DateOnly.FromDateTime(nextFromDateTime).AddDays(1); + nextFromDateTime = new(startDate, fromTime); + nextToDateTime = nextToDateTime.AddDays(1); + } + return nextFromDateTime - now; + } + } + } + + /// Initialize instance with arguments given. + [JsonConstructor] + public User( + string name, string tenantId, string clientId, string? secret, FileInfo? certificate, + TimeOnly? fromTime, TimeOnly? toTime, ImmutableList? days + ) => (this.name, this.tenantId, this.clientId, this.secret, this.certificate, this.fromTime, this.toTime, this.days) = + (name, tenantId, clientId, secret, certificate, fromTime, toTime, days); + + } +} diff --git a/E5Renewer/Models/Secrets/Yaml/YamlSerializeCompatibleUser.cs b/E5Renewer/Models/Secrets/Yaml/YamlSerializeCompatibleUser.cs new file mode 100644 index 0000000..b0cc52d --- /dev/null +++ b/E5Renewer/Models/Secrets/Yaml/YamlSerializeCompatibleUser.cs @@ -0,0 +1,50 @@ +using System.Collections.Immutable; + +using VYaml.Annotations; + +namespace E5Renewer.Models.Secrets.Yaml +{ + [YamlObject(NamingConvention.SnakeCase)] + internal partial class YamlSerializeCompatibleUser + { + [YamlIgnore] + public User value + { + get + { + FileInfo? certificate = this.certificate is not null ? new(this.certificate) : null; + ImmutableList? days = + this.days is not null + ? ImmutableList.CreateRange(this.days.Select((day) => (DayOfWeek)day)) + : null; + return new( + this.name, this.tenantId, this.clientId, this.secret, certificate, + this.fromTime, this.toTime, days + ); + } + } + + public string name { get; } + + public string tenantId { get; } + + public string clientId { get; } + + public string? secret { get; } + + public string? certificate { get; } + + public TimeOnly? fromTime { get; } + + public TimeOnly? toTime { get; } + + public List? days { get; } + + [YamlConstructor] + public YamlSerializeCompatibleUser( + string name, string tenantId, string clientId, + string? secret, string? certificate, TimeOnly? fromTime, TimeOnly? toTime, List? days + ) => (this.name, this.tenantId, this.clientId, this.secret, this.certificate, this.fromTime, this.toTime, this.days) = + (name, tenantId, clientId, secret, certificate, fromTime, toTime, days); + } +} diff --git a/E5Renewer/Models/Secrets/Yaml/YamlSerializeCompatibleUserSecret.cs b/E5Renewer/Models/Secrets/Yaml/YamlSerializeCompatibleUserSecret.cs new file mode 100644 index 0000000..e497651 --- /dev/null +++ b/E5Renewer/Models/Secrets/Yaml/YamlSerializeCompatibleUserSecret.cs @@ -0,0 +1,27 @@ +using System.Collections.Immutable; + +using VYaml.Annotations; + +namespace E5Renewer.Models.Secrets.Yaml +{ + [YamlObject(NamingConvention.SnakeCase)] + internal partial class YamlSerializeCompatibleUserSecret + { + [YamlIgnore] + public UserSecret value + { + get => new( + ImmutableList.CreateRange(users.Select((user) => user.value)), + passwords is not null ? ImmutableDictionary.CreateRange(passwords) : null + ); + } + + public List users { get; } + + public Dictionary? passwords { get; } + + [YamlConstructor] + public YamlSerializeCompatibleUserSecret(List users, Dictionary? passwords) => + (this.users, this.passwords) = (users, passwords); + } +} diff --git a/E5Renewer/Models/Secrets/Yaml/YamlUserSecretLoader.cs b/E5Renewer/Models/Secrets/Yaml/YamlUserSecretLoader.cs new file mode 100644 index 0000000..93c2a28 --- /dev/null +++ b/E5Renewer/Models/Secrets/Yaml/YamlUserSecretLoader.cs @@ -0,0 +1,38 @@ +using E5Renewer.Models.Modules; + +using VYaml.Serialization; + +namespace E5Renewer.Models.Secrets.Yaml +{ + /// Class for loading user secret from yaml file. + [Module] + public class YamlUserSecretLoader : IUserSecretLoader + { + /// + public string name { get => nameof(YamlUserSecretLoader); } + + /// + public string author { get => "E5Renewer"; } + + /// + public SemanticVersioning.Version apiVersion + { + get => typeof(YamlUserSecretLoader).Assembly.GetName().Version?.ToSemanticVersion() ?? new(0, 1, 0); + } + + /// + public bool IsSupported(FileInfo userSecret) => userSecret.Name.EndsWith(".yml") || userSecret.Name.EndsWith(".yaml"); + + /// + public async Task LoadSecretAsync(FileInfo userSecret) + { + YamlSerializeCompatibleUserSecret userSecretObject; + using (FileStream fileStream = userSecret.OpenRead()) + { + userSecretObject = await YamlSerializer.DeserializeAsync(fileStream); + } + return userSecretObject.value; + } + + } +} diff --git a/E5Renewer/Models/UintExtends.cs b/E5Renewer/Models/UintExtends.cs deleted file mode 100644 index 0d5f714..0000000 --- a/E5Renewer/Models/UintExtends.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace E5Renewer.Models -{ - /// Extended methods to - /// uint. - /// - public static class UintExtends - { - private const uint minPermission = 0; // 0o000 - private const uint maxPermission = 511; // 0o777 - - /// Convert - /// uint - /// to - /// UnixFileMode. - /// - public static UnixFileMode ToUnixFileMode(this uint permission) - { - - if (permission < UintExtends.minPermission) - { - permission = UintExtends.minPermission; - } - - if (permission > UintExtends.maxPermission) - { - permission = UintExtends.maxPermission; - } - - return (UnixFileMode)permission; - } - } -} diff --git a/E5Renewer/Program.cs b/E5Renewer/Program.cs index 922dd61..063ce85 100644 --- a/E5Renewer/Program.cs +++ b/E5Renewer/Program.cs @@ -1,408 +1,177 @@ using System.Net; -using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reflection; using System.Text.Json; using E5Renewer; using E5Renewer.Controllers; -using E5Renewer.Models; -using E5Renewer.Models.BackgroundServices; -using E5Renewer.Models.Config; -using E5Renewer.Models.GraphAPIs; using E5Renewer.Models.Modules; -using E5Renewer.Models.Statistics; using Microsoft.AspNetCore.Mvc; -/// -public static class Program +const UnixFileMode defaultListenUnixSocketPermission = + UnixFileMode.UserRead | UnixFileMode.UserWrite | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite; +bool setSocketPermission = false; + +// Variables from Configuration +bool systemd; +IPEndPoint? listenTcpSocket; +string? listenUnixSocketPath; +UnixFileMode listenUnixSocketPermission; +FileInfo? userSecret; +FileInfo? tokenFile; +string? token; + +Dictionary commandLineSwitchMap = new() { - private const string timeStampFormat = "yyyy-MM-dd HH:mm:ss "; - private static readonly List modulesCheckers = new(); - private static readonly List configParsers = new(); - private static readonly List aspNetModules = new(); - private static ILogger logger = null!; - - /// Start E5Renewer. - /// The path to config file. - /// If program runs in systemd environment. - public static async Task Main(FileInfo config, bool systemd = false) - { - string env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; - LogLevel minimumLevel = env == "Debug" ? LogLevel.Debug : LogLevel.Information; - Program.logger = LoggerFactory.Create( - (configure) => - { - configure.ClearProviders(); - if (systemd) - { - configure.AddSystemdConsole((config) => config.TimestampFormat = Program.timeStampFormat); - } - else - { - configure.AddSimpleConsole((config) => - { - config.SingleLine = true; - config.TimestampFormat = Program.timeStampFormat; - }); - } - configure.SetMinimumLevel(minimumLevel); - } - ).CreateLogger(nameof(Program)); - - Program.modulesCheckers.Clear(); - Program.configParsers.Clear(); - Program.aspNetModules.Clear(); - - Program.DiscoverModules(Assembly.GetExecutingAssembly()); - IEnumerable assemblies = GetPossibleModulesPaths(). - Select( - (directory) => - { - FileInfo[] files = directory.GetFiles(directory.Name + ".dll", SearchOption.TopDirectoryOnly); - if (files.Count() > 0) - { - FileInfo file = files[0]; - ModuleLoadContext context = new(file); - string assemblyName = Path.GetFileNameWithoutExtension(file.FullName); - try - { - Assembly assembly = context.LoadFromAssemblyName( - new(assemblyName) - ); - Program.logger.LogDebug("Load assembly from {0} success.", assemblyName); - return assembly; - } - catch (Exception e) - { - Program.logger.LogError("Failed to load assembly because {0}.", e.Message); - } - } - return null; - } - ).OfType(); - Program.DiscoverModules(assemblies.ToArray()); - - Program.CheckModules(); - - await Program.LaunchServerAsync(await Program.ParseConfigAsync(config), systemd); - } - - private static void CheckModules() + {"--systemd", nameof(systemd)}, + {"--user-secret", nameof(userSecret)}, + {"--token", nameof(token)}, + {"--token-file", nameof(tokenFile)}, + {"--listen-tcp-socket", nameof(listenTcpSocket)}, + {"--listen-unix-socket-path", nameof(listenUnixSocketPath)}, + {"--listen-unix-socket-permission", nameof(listenUnixSocketPermission)} +}; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.Configuration.AddCommandLine(args, commandLineSwitchMap); + +systemd = args.ContainsFlag(nameof(systemd)) || builder.Configuration.GetValue(nameof(systemd)); +listenTcpSocket = builder.Configuration.GetValue(nameof(listenTcpSocket))?.AsIPEndPoint(); +listenUnixSocketPath = builder.Configuration.GetValue(nameof(listenUnixSocketPath)); +listenUnixSocketPermission = builder.Configuration.GetValue( + nameof(listenUnixSocketPermission), defaultListenUnixSocketPermission); +userSecret = builder.Configuration.GetValue(nameof(userSecret))?.AsFileInfo(); +tokenFile = builder.Configuration.GetValue(nameof(tokenFile))?.AsFileInfo(); +token = builder.Configuration.GetValue(nameof(token)); + +builder.Logging.AddConsole(systemd, builder.Environment.IsDevelopment() ? LogLevel.Debug : LogLevel.Information); + +builder.WebHost.ConfigureKestrel( +(kestrelServerOptions) => { - foreach (IConfigParser parser in Program.configParsers) + if (listenTcpSocket is not null) { - foreach (IModulesChecker checker in Program.modulesCheckers) - { - Program.logger.LogDebug("Checking module {0} with checker {1}...", parser.name, checker.name); - checker.CheckModules(parser); - } + kestrelServerOptions.Listen(listenTcpSocket); } - } - private static async ValueTask ParseConfigAsync(FileInfo config) - { - Config runtimeConfig; - try - { - IConfigParser parser = Program.configParsers.First((parser) => parser.IsSupported(config)); - runtimeConfig = await parser.ParseConfigAsync(config); - } - catch (InvalidOperationException) - { - Program.logger.LogError("Failed to find parser for config {0}.", config); - throw; - } - catch (Exception e) + if (listenUnixSocketPath is not null && Socket.OSSupportsUnixDomainSockets) { - Program.logger.LogError("Failed to parse config {0} because {1}.", config, e.Message); - throw; + kestrelServerOptions.ListenUnixSocket(listenUnixSocketPath); + setSocketPermission = true; } - return runtimeConfig; } +); - private static async ValueTask LaunchServerAsync(Config config, bool systemd) +builder.Services.AddModules(Assembly.GetExecutingAssembly()); +IEnumerable assemblies = GetPossibleModulesPaths(). +Select( + (directory) => { - if (!config.isCheckPassed) + FileInfo[] files = directory.GetFiles(directory.Name + ".dll", SearchOption.TopDirectoryOnly); + if (files.Count() > 0) { - throw new InvalidDataException("config check failed."); - } - bool setSocketPermission = false; - WebApplicationBuilder builder = WebApplication.CreateBuilder(); - builder.Logging.ClearProviders(); - if (systemd) - { - builder.Logging.AddSystemdConsole((config) => config.TimestampFormat = Program.timeStampFormat); - } - else - { - builder.Logging.AddSimpleConsole( - (config) => - { - config.SingleLine = true; config.TimestampFormat = Program.timeStampFormat; - } - ); - } - builder.WebHost.ConfigureKestrel( - (configure) => + ModuleLoadContext context = new(files[0]); + try { - const uint minPort = 1; - const uint maxPort = 65535; - - bool portValid = - config.listenPort >= minPort && - config.listenPort <= maxPort && - !Program.IsPortUsed(config.listenPort); - - bool listenHttp = - IPAddress.TryParse(config.listenAddr, out IPAddress? listenAddress) && - portValid; - if (listenHttp && listenAddress is not null) - { - configure.Listen( - listenAddress, - (int)config.listenPort - ); - } - else if (Socket.OSSupportsUnixDomainSockets) - { - configure.ListenUnixSocket(config.listenSocket); - setSocketPermission = true; - } - else - { - throw new Exception("Cannot Bind to HTTP or Unix Domain Socket."); - } + Assembly assembly = context.LoadFromAssemblyName( + new(Path.GetFileNameWithoutExtension(files[0].FullName)) + ); + return assembly; } - ); - - builder.Services.ApplyRuntimeConfig(config); - - WebApplication app = builder.Build(); - app.UseExceptionHandler( - (exceptionHandlerApp) => exceptionHandlerApp.Run( - async (context) => - { - InvokeResult result = await GenerateDummyResult(context); - await context.Response.WriteAsJsonAsync(result); - } - ) - ); - app.UseRouting(); - string[] allowedMethods = ["GET", "POST"]; - app.Logger.LogDebug("Setting allowed method to {0}", string.Join(", ", allowedMethods)); - app.UseHttpMethodChecker(allowedMethods); - app.Logger.LogDebug("Setting check Unix timestamp in request"); - app.UseUnixTimestampChecker(); - app.Logger.LogDebug("Setting authToken"); - app.UseAuthTokenAuthentication(config.authToken); - app.Logger.LogDebug("Mapping controllers"); - app.MapControllers(); - await app.StartAsync(); - if (setSocketPermission && !OperatingSystem.IsWindows()) - { - File.SetUnixFileMode(config.listenSocket, config.listenSocketPermission.ToUnixFileMode()); + catch { } } - await app.WaitForShutdownAsync(); + return null; } +).OfType(); +builder.Services.AddModules(assemblies.ToArray()); - private static IServiceCollection ApplyRuntimeConfig(this IServiceCollection services, Config config) - { - if (config.isCheckPassed) - { - if (config.passwords is not null) - { - services.AddSingleton(config.passwords); - } - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHostedService(); - services.AddHostedService(); - foreach (IModulesChecker checker in Program.modulesCheckers) - { - services.AddSingleton(checker); - } - Random random = new(); - IEnumerable graphAPICallers = Program.aspNetModules.OfType(); - IEnumerable otherAspNetModules = Program.aspNetModules.Where((module) => (module as IGraphAPICaller) is null); - foreach (GraphUser user in config.users) - { - services.AddSingleton(user); - IGraphAPICaller caller = random.GetItems(graphAPICallers.ToArray(), 1)[0]; - services.AddKeyedSingleton(user, caller); - } - foreach (IAspNetModule module in otherAspNetModules) - { - services.AddSingleton(module); - } - IEnumerable apiFunctionsTypes = Assembly.GetExecutingAssembly().GetTypes().GetNonAbstractClassesAssainableTo(); - foreach (Type t in apiFunctionsTypes) - { - logger.LogDebug("Registering {0} as {1}", t.FullName, nameof(IAPIFunction)); - services.AddSingleton(typeof(IAPIFunction), t); - } - } +if (userSecret is not null) +{ + builder.Services.AddUserSecretFile(userSecret); +} +else +{ + throw new NullReferenceException(nameof(userSecret)); +} - services.AddControllers( - ).AddJsonOptions( - (options) => - { - options.JsonSerializerOptions.WriteIndented = true; - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - } - ).ConfigureApiBehaviorOptions( - (options) => - { - options.InvalidModelStateResponseFactory = (actionContext) => +builder.Services + .AddTokenOverride(token, tokenFile) + .AddDummyResultGenerator() + .AddSecretProvider() + .AddStatusManager() + .AddTimeStampGenerator() + .AddAPIFunctionImplementations() + .AddHostedServices() + .AddControllers() + .AddJsonOptions( + (options) => + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + ) + .ConfigureApiBehaviorOptions( + (options) => + options.InvalidModelStateResponseFactory = + (actionContext) => { - InvokeResult result = Program.GenerateDummyResult(actionContext.HttpContext).Result; + InvokeResult result = actionContext.HttpContext.RequestServices.GetRequiredService() + .GenerateDummyResult(actionContext.HttpContext); return new JsonResult(result); - }; - } - ); - return services; - } - - private static void DiscoverModules(params Assembly[] assemblies) - { - foreach (Assembly assembly in assemblies) - { - foreach (Type t in Program.GetModules(assembly)) - { - IModule? instance = null; - try - { - instance = Activator.CreateInstance(t) as IModule; - } - catch (Exception e) - { - Program.logger.LogError("Failed to create instance for module {0} because {1}.", t.FullName, e.Message); - } - - bool discovered = false; - if (instance is IModulesChecker moduleChecker) - { - Program.logger.LogDebug( - "Found modules checker {0} with name {1}.", - moduleChecker.GetType().FullName, - moduleChecker.name - ); - modulesCheckers.Add(moduleChecker); - discovered = true; - } - - if (instance is IConfigParser configParser) - { - Program.logger.LogDebug( - "Found config parser {0} with name {1}.", - configParser.GetType().FullName, - configParser.name - ); - configParsers.Add(configParser); - discovered = true; } + ); - if (instance is IAspNetModule aspNetModule) - { - Program.logger.LogDebug( - "Found aspnet module {0} with name {1}.", - aspNetModule.GetType().FullName, - aspNetModule.name - ); - aspNetModules.Add(aspNetModule); - discovered = true; - } - if (!discovered && instance is not null) - { - Program.logger.LogWarning( - "Found unknown module {0} with name {1}, ignoring", - instance.GetType().FullName, - instance.name); - } - } - } - } +WebApplication app = builder.Build(); - private static IEnumerable GetModules(Assembly assembly) - { - return assembly.GetTypes().GetNonAbstractClassesAssainableTo().Where( - (t) => t.IsDefined(typeof(ModuleAttribute)) - ); - } +app.UseModulesCheckers(); - private static IEnumerable GetPossibleModulesPaths() - { - const string modulesBaseFolderName = "modules"; - const string modulesBaseFileName = "E5Renewer.Modules.*.dll"; - Dictionary> results = new(); - DirectoryInfo assemblyDirectory = new(Path.Combine(AppContext.BaseDirectory, modulesBaseFolderName)); - DirectoryInfo[] directoriesToCheck = [assemblyDirectory]; - foreach (DirectoryInfo currentDirectory in directoriesToCheck) - { - if (currentDirectory.Exists && !results.ContainsKey(currentDirectory.FullName)) - { - // /modules/*/E5Renewer.Modules.*/E5Renewer.Modules.*.dll - IEnumerable directories = currentDirectory.GetFiles(modulesBaseFileName, SearchOption.AllDirectories).Where( - (fileInfo) => (fileInfo.Directory?.Name ?? string.Empty) == fileInfo.Name.Substring(0, fileInfo.Name.Length - 4) - ).Select((x) => x.Directory).OfType(); - results[currentDirectory.FullName] = directories; - } - } - return results.Values.SelectMany((x) => x); - } - private static bool IsPortUsed(uint port) - { - IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); - TcpConnectionInformation[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpConnections(); - foreach (TcpConnectionInformation info in tcpConnInfoArray) - { - if (info.LocalEndPoint.Port == port) +app.UseExceptionHandler( + (exceptionHandlerApp) => + exceptionHandlerApp.Run( + async (context) => { - return true; + InvokeResult result = await context.RequestServices.GetRequiredService() + .GenerateDummyResultAsync(context); + await context.Response.WriteAsJsonAsync(result); } - } - return false; - } - private static async ValueTask GenerateDummyResult(HttpContext context) + ) + +); +app.UseRouting(); +string[] allowedMethods = ["GET", "POST"]; +app.Logger.LogDebug("Setting allowed method to {0}", string.Join(", ", allowedMethods)); +app.UseHttpMethodChecker(allowedMethods); +app.Logger.LogDebug("Setting check Unix timestamp in request"); +app.UseUnixTimestampChecker(); +app.Logger.LogDebug("Setting authToken"); +app.UseAuthTokenAuthentication(); +app.Logger.LogDebug("Mapping controllers"); +app.MapControllers(); +await app.StartAsync(); +if (setSocketPermission && !OperatingSystem.IsWindows() && listenUnixSocketPath is not null) +{ + File.SetUnixFileMode(listenUnixSocketPath, listenUnixSocketPermission); +} +await app.WaitForShutdownAsync(); + +static IEnumerable GetPossibleModulesPaths() +{ + const string modulesBaseFolderName = "modules"; + const string modulesBaseFileName = "E5Renewer.Modules.*.dll"; + Dictionary> results = new(); + DirectoryInfo assemblyDirectory = new(Path.Combine(AppContext.BaseDirectory, modulesBaseFolderName)); + DirectoryInfo[] directoriesToCheck = [assemblyDirectory]; + foreach (DirectoryInfo currentDirectory in directoriesToCheck) { - IUnixTimestampGenerator unixTimestampGenerator = context.RequestServices.GetRequiredService(); - Dictionary queries; - switch (context.Request.Method) - { - case "GET": - queries = context.Request.Query.Select( - (kv) => new KeyValuePair(kv.Key, kv.Value.FirstOrDefault() as object) - ).ToDictionary(); - break; - case "POST": - byte[] buffer = new byte[context.Request.ContentLength ?? context.Request.Body.Length]; - int length = await context.Request.Body.ReadAsync(buffer); - byte[] contents = buffer.Take(length).ToArray(); - queries = JsonSerializer.Deserialize>(contents) ?? new(); - break; - default: - queries = new(); - break; - } - if (queries.ContainsKey("timestamp")) + if (currentDirectory.Exists && !results.ContainsKey(currentDirectory.FullName)) { - queries.Remove("timestamp"); + // /modules/*/E5Renewer.Modules.*/E5Renewer.Modules.*.dll + IEnumerable directories = currentDirectory.GetFiles(modulesBaseFileName, SearchOption.AllDirectories).Where( + (fileInfo) => (fileInfo.Directory?.Name ?? string.Empty) == fileInfo.Name.Substring(0, fileInfo.Name.Length - 4) + ).Select((x) => x.Directory).OfType(); + results[currentDirectory.FullName] = directories; } - string fullPath = context.Request.PathBase + context.Request.Path; - int lastOfSlash = fullPath.LastIndexOf("/"); - int firstOfQuote = fullPath.IndexOf("?"); - string methodName = - firstOfQuote > lastOfSlash ? - fullPath.Substring(lastOfSlash + 1, firstOfQuote - lastOfSlash) : - fullPath.Substring(lastOfSlash + 1); - return new InvokeResult( - methodName, - queries, - null, - unixTimestampGenerator.GetUnixTimestamp() - ); } + return results.Values.SelectMany((x) => x); } diff --git a/E5Renewer/StringArrayExtends.cs b/E5Renewer/StringArrayExtends.cs new file mode 100644 index 0000000..05372dc --- /dev/null +++ b/E5Renewer/StringArrayExtends.cs @@ -0,0 +1,26 @@ +namespace E5Renewer +{ + internal static class StringArrayExtends + { + public static bool ContainsFlag(this string[] args, string flag) + { + string[] prefixes = ["--", "-", "/"]; + return prefixes.Any((prefix) => args.ContainsFlag(flag, prefix)); + } + + public static bool ContainsFlag(this string[] args, string flag, string prefix) + { + if (flag.Length <= 0) + { + throw new InvalidDataException($"{nameof(flag.Length)} of {nameof(flag)} is not greater than 0"); + } + bool allPrefixes = flag.Chunk(prefix.Length) + .All((cs) => prefix.Contains(string.Concat(cs))); + if (allPrefixes) + { + throw new InvalidDataException($"{nameof(flag)} is invalid"); + } + return args.Contains($"{prefix}{flag}"); + } + } +} diff --git a/E5Renewer/StringExtends.cs b/E5Renewer/StringExtends.cs new file mode 100644 index 0000000..91dc198 --- /dev/null +++ b/E5Renewer/StringExtends.cs @@ -0,0 +1,11 @@ +using System.Net; + +namespace E5Renewer +{ + internal static class StringExtends + { + public static FileInfo AsFileInfo(this string s) => new(s); + + public static IPEndPoint? AsIPEndPoint(this string s) => IPEndPoint.TryParse(s, out IPEndPoint? result) ? result : null; + } +} diff --git a/E5Renewer/TypeArrayExtends.cs b/E5Renewer/TypeArrayExtends.cs index 0985c27..6ce06c6 100644 --- a/E5Renewer/TypeArrayExtends.cs +++ b/E5Renewer/TypeArrayExtends.cs @@ -1,7 +1,7 @@ namespace E5Renewer { /// Extends to Type[] - public static class TypeArrayExtends + internal static class TypeArrayExtends { /// Get types which is not abstract and is assainabble to T. /// The array of types. diff --git a/E5Renewer/WebApplicationExtends.cs b/E5Renewer/WebApplicationExtends.cs index 4251fa5..624ed1d 100644 --- a/E5Renewer/WebApplicationExtends.cs +++ b/E5Renewer/WebApplicationExtends.cs @@ -1,38 +1,69 @@ using System.Text.Json; +using E5Renewer.Controllers; +using E5Renewer.Models.GraphAPIs; +using E5Renewer.Models.Modules; +using E5Renewer.Models.Secrets; using E5Renewer.Models.Statistics; using Microsoft.Extensions.Primitives; namespace E5Renewer { - /// Extends to WebApplication. - public static class WebApplicationExtends + internal static class WebApplicationExtends { - /// Use custom authentication middleware. - /// The WebApplication instance. - /// The token used for authentication. - public static IApplicationBuilder UseAuthTokenAuthentication(this WebApplication app, string authToken) + public static IApplicationBuilder UseModulesCheckers(this WebApplication app) + { + IEnumerable modulesCheckers = app.Services.GetServices(); + IEnumerable userSecretLoaders = app.Services.GetServices(); + IEnumerable graphAPICallers = app.Services.GetServices(); + IEnumerable otherModules = app.Services.GetServices(); + + List modulesToCheck = new(); + modulesToCheck.AddRange(modulesCheckers); + modulesToCheck.AddRange(userSecretLoaders); + modulesToCheck.AddRange(graphAPICallers); + modulesToCheck.AddRange(otherModules); + + modulesToCheck.ForEach( + (m) => + { + foreach (IModulesChecker checker in modulesCheckers) + { + + try + { + checker.CheckModules(m); + } + catch { } + } + } + ); + + return app; + } + + public static IApplicationBuilder UseAuthTokenAuthentication(this WebApplication app) { return app.Use( async (context, next) => { + ISecretProvider secretProvider = app.Services.GetRequiredService(); + IDummyResultGenerator dummyResultGenerator = app.Services.GetRequiredService(); const string authenticationHeaderName = "Authentication"; string? authentication = context.Request.Headers[authenticationHeaderName].FirstOrDefault(); - if (authentication == authToken) + if (authentication == await secretProvider.GetRuntimeTokenAsync()) { await next(); return; } context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsync("Authenticate failed"); + InvokeResult result = await dummyResultGenerator.GenerateDummyResultAsync(context); + await context.Response.WriteAsJsonAsync(result); } ); } - /// Only allow methods given to connect. - /// The WebApplication instance. - /// The request methods to allow. public static IApplicationBuilder UseHttpMethodChecker(this WebApplication app, params string[] methods) { return app.Use( @@ -49,9 +80,6 @@ public static IApplicationBuilder UseHttpMethodChecker(this WebApplication app, ); } - /// Check timestamp in request. - /// The WebApplication instance. - /// Max allowed seconds. public static IApplicationBuilder UseUnixTimestampChecker(this WebApplication app, uint allowedMaxSeconds = 30) { return app.Use( @@ -75,7 +103,8 @@ public static IApplicationBuilder UseUnixTimestampChecker(this WebApplication ap return; } context.Response.StatusCode = StatusCodes.Status403Forbidden; - await context.Response.WriteAsync("Request is outdated"); + InvokeResult result = await app.Services.GetRequiredService().GenerateDummyResultAsync(context); + await context.Response.WriteAsJsonAsync(result); } ); } diff --git a/README.md b/README.md index c7e5e0d..0cb8898 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,15 @@ A tool to renew e5 subscription by calling msgraph APIs 2. Create Configuration - Copy [`config.json.example`](./config.json.example) to `config.json`, edit it as your need. You can always add more credentials. Please edit `auth_token` so only people you authenticated can access the statistics. - - We will listen on tcp socket by default, if `listen_addr` is an empty string and your platform supports Unix domain socket, we will listen on Unix domain socket with path `listen_socket` and permission `listen_socket_permission`. + Copy [`config.json.example`](./config.json.example) to `config.json`, edit it as your need. You can always add more credentials. If you want to use certificate instead secret, which is better for security, you can write a `certificate` key with path to your certificate file instead `secret` key. + If we find you set `certificate`, it will always be used instead `secret`. - Tips: We support json, yaml and toml formats, just let their contents be equal, the configuration result is same. + Setting days is needed to be cautious, as it means `DayOfWeek` in program, + check [here](https://learn.microsoft.com/en-us/dotnet/api/system.dayofweek#fields) to find out its correct value. - > [!NOTE] - > Due to that C# does not have a native octal number support, we use the `listen_socket_permission` as unix permission directly. - > For example, if you are using json format, you need to set it to `438` in order to see socket mode is `rw-rw-rw-`. + Tips: We support json, yaml and toml formats, just let their contents be equal, the configuration result is same. 3. Install .NET @@ -53,7 +51,28 @@ A tool to renew e5 subscription by calling msgraph APIs 5. Run program - Simply run `./E5Renewer` in binaries folder. + Simply run `./E5Renewer` in binaries folder with arguments needed. + + Here are supported arguments: + + - `--systemd`: If runs in systemd environment, most of the time you should not need it. + - `--user-secret`: The path to the user secret file. User secret file is used to storage sensitive information, so please keep it safe. + - `--token`: The string to access json api, please keep it safe. + - `--token-file`: The file which contains token, please keep that file safe. + - `--listen-tcp-socket`: The tcp socket to listen instead default one(`127.0.0.1:5000`). + - `--listen-unix-socket-path`: The path to create a unix domain socket to access json api. + - `--listen-inux-socket-permission`: The permission to the unix domain socket file. + - All AspNet.Core supported arguments. See [here](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/#command-line) for more info. + + We will listen on tcp socket `127.0.0.1:5000` by default, if you want to customize it, + you need to set commandline argument `--listen-tcp-socket` like `--listen-tcp-socket=127.0.0.1:8888`. + You can also choose listen unix domain socket by setting commandline argument like `--listen-unix-socket-path=/path/to/socket` + and set socket file permission with argument like `--listen-unix-socket-permission=511`. + + > [!NOTE] + > If `--token` and `--token-file` both are specified, we prefer `--token`. If you forget to set nither of them, we use a randomly generated value. + > You can find it out in log output after sending any request to the program and meeting a authentication error. + > If you want to set unix socket permission, you have to write its actual value instead octal format. For example, using `511` instead `777` is required. ## Get running statistics @@ -61,6 +80,7 @@ Using `curl` or any tool which can send http request, send request to `http://`. You will get json response if everything is fine. If it is a GET request, send milisecond timestamp in query param `timestamp`, If it is a POST request, send milisecond timestamp in post json with key `timestamp` and convert it to string. +Most of the time, we will return json instead plain text, but you need to check response code to see if request is success. For example: diff --git a/config.json.example b/config.json.example index 8ee56f0..21419a8 100644 --- a/config.json.example +++ b/config.json.example @@ -1,9 +1,4 @@ { - "auth_token": "example-auth-token", - "listen_addr": "127.0.0.1", - "listen_port": 8888, - "listen_socket": "/run/e5renewer/e5renewer.socket", - "listen_socket_permission": 666, "users": [ { "name": "e5renewer", @@ -11,7 +6,8 @@ "client_id": "", "secret": "", "from_time": "00:00:00", - "to_time": "23:59:59" + "to_time": "23:59:59", + "days": [1,2,3] } ] } \ No newline at end of file diff --git a/e5renewer.service b/e5renewer.service index b5ddbe8..5bacc32 100644 --- a/e5renewer.service +++ b/e5renewer.service @@ -4,17 +4,22 @@ Wants=network-online.target After=network-online.target [Service] -ExecStart=E5Renewer -c /etc/e5renewer/config.json --systemd +LoadCredential=user-secret.json:%E/e5renewer/user-secret.json +LoadCredential=token.txt:%E/e5renewer/token.txt +ExecStart=/usr/bin/E5Renewer \ + --user-secret=%d/user-secret.json \ + --token-file=%d/token.txt \ + --systemd User=e5renewer DynamicUser=yes +RuntimeDirectory=e5renewer + NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes -RuntimeDirectory=e5renewer -ConfigurationDirectory=e5renewer PrivateDevices=yes PrivateNetwork=yes PrivateUsers=yes