Compare commits
7 Commits
6f6a37d5a6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f0019592e | |||
| 4c271241d7 | |||
| 9f7cb79bad | |||
| a94abca127 | |||
| 96f0112449 | |||
| 7380680230 | |||
| b5cbad3664 |
@@ -24,9 +24,9 @@ jobs:
|
|||||||
http = true
|
http = true
|
||||||
insecure = true
|
insecure = true
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-dotnet@v3
|
- uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9' # SDK Version to use.
|
dotnet-version: '10' # SDK Version to use.
|
||||||
dotnet-quality: 'ga'
|
dotnet-quality: 'ga'
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: '**/packages.lock.json'
|
cache-dependency-path: '**/packages.lock.json'
|
||||||
@@ -50,10 +50,3 @@ jobs:
|
|||||||
--configuration Release --os linux --arch x64 \
|
--configuration Release --os linux --arch x64 \
|
||||||
/t:PublishContainer -p ContainerRegistry=${{ vars.DOCKER_GITEA_DOMAIN }} \
|
/t:PublishContainer -p ContainerRegistry=${{ vars.DOCKER_GITEA_DOMAIN }} \
|
||||||
-p ContainerRepository=ebolo/caddy-manager -p:ContainerImageTags='"${{ steps.metadata.outputs.APP_VERSION }};latest"'
|
-p ContainerRepository=ebolo/caddy-manager -p:ContainerImageTags='"${{ steps.metadata.outputs.APP_VERSION }};latest"'
|
||||||
- name: Deploy to Komodo
|
|
||||||
uses: fjogeleit/http-request-action@v1
|
|
||||||
if: success()
|
|
||||||
with:
|
|
||||||
url: '${{ vars.WINDMILL_DOMAIN }}/komodo/pull-stack/${{ secrets.KOMODO_STACK_ID }}'
|
|
||||||
method: 'PUT'
|
|
||||||
customHeaders: '{"Auth-Key": "${{ secrets.WINDMILL_KEY }}"}'
|
|
||||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -25,12 +25,14 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-dotnet@v3
|
- uses: actions/setup-dotnet@v3
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9' # SDK Version to use.
|
dotnet-version: '10' # SDK Version to use.
|
||||||
dotnet-quality: 'ga'
|
dotnet-quality: 'ga'
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: '**/packages.lock.json'
|
cache-dependency-path: '**/packages.lock.json'
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore --locked-mode
|
# Scoped to the web app so the test project is never restored or built here; it pulls in
|
||||||
|
# Contracts and Services through its project references. Tests run locally, not in this job.
|
||||||
|
run: dotnet restore CaddyManager/CaddyManager.csproj --locked-mode
|
||||||
- name: Application metadata
|
- name: Application metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
run: |
|
run: |
|
||||||
@@ -41,7 +43,7 @@ jobs:
|
|||||||
sed -i "s/public static readonly string CommitHash = \"\[DEVELOPMENT\]\";/public static readonly string CommitHash = \"${{ steps.metadata.outputs.COMMIT_HASH }}\";/" CaddyManager/Configurations/Application/ApplicationInfo.cs
|
sed -i "s/public static readonly string CommitHash = \"\[DEVELOPMENT\]\";/public static readonly string CommitHash = \"${{ steps.metadata.outputs.COMMIT_HASH }}\";/" CaddyManager/Configurations/Application/ApplicationInfo.cs
|
||||||
- name: Publish container
|
- name: Publish container
|
||||||
run: |
|
run: |
|
||||||
dotnet publish \
|
dotnet publish CaddyManager/CaddyManager.csproj \
|
||||||
--configuration Release --os linux --arch x64 \
|
--configuration Release --os linux-musl --arch x64 \
|
||||||
/t:PublishContainer -p ContainerRegistry=ghcr.io \
|
/t:PublishContainer -p ContainerRegistry=ghcr.io \
|
||||||
-p ContainerRepository=${{ github.repository }} -p:ContainerImageTags='"${{ steps.metadata.outputs.APP_VERSION }};latest"'
|
-p ContainerRepository=${{ github.repository }} -p:ContainerImageTags='"${{ steps.metadata.outputs.APP_VERSION }};latest"'
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ public class CaddyService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.FileName != CaddyGlobalConfigName && IsInvalidFileName(request.FileName))
|
||||||
|
{
|
||||||
|
return Failure("The configuration file name contains invalid characters");
|
||||||
|
}
|
||||||
|
|
||||||
var filePath = Path.Combine(Configurations.ConfigDir,
|
var filePath = Path.Combine(Configurations.ConfigDir,
|
||||||
request.FileName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{request.FileName}.caddy");
|
request.FileName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{request.FileName}.caddy");
|
||||||
// if in the new mode, we would have to check if the file already exists
|
// if in the new mode, we would have to check if the file already exists
|
||||||
@@ -198,6 +203,12 @@ public class CaddyService(
|
|||||||
|
|
||||||
foreach (var configurationName in configurationNames)
|
foreach (var configurationName in configurationNames)
|
||||||
{
|
{
|
||||||
|
if (configurationName != CaddyGlobalConfigName && IsInvalidFileName(configurationName))
|
||||||
|
{
|
||||||
|
failed.Add(configurationName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var filePath = Path.Combine(Configurations.ConfigDir,
|
var filePath = Path.Combine(Configurations.ConfigDir,
|
||||||
configurationName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{configurationName}.caddy");
|
configurationName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{configurationName}.caddy");
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.2.1" />
|
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.2.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.10" />
|
||||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||||
<PackageReference Include="Humanizer" Version="3.0.0-beta.96" />
|
<PackageReference Include="Humanizer" Version="3.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ public class ConfigurationsService(IConfiguration configuration) : IConfiguratio
|
|||||||
else if (section.EndsWith("Configuration"))
|
else if (section.EndsWith("Configuration"))
|
||||||
section = section[..^"Configuration".Length];
|
section = section[..^"Configuration".Length];
|
||||||
|
|
||||||
return configuration.GetSection(section).Get<T>() ?? Activator.CreateInstance<T>();
|
var result = configuration.GetSection(section).Get<T>();
|
||||||
|
var defaults = Activator.CreateInstance<T>();
|
||||||
|
|
||||||
|
if (result is null) return defaults;
|
||||||
|
|
||||||
|
// The .NET 10 binder writes nulls over property initialisers, so put the defaults back
|
||||||
|
foreach (var property in typeof(T).GetProperties())
|
||||||
|
{
|
||||||
|
if (property is { CanRead: true, CanWrite: true } && property.GetValue(result) is null)
|
||||||
|
property.SetValue(result, property.GetValue(defaults));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<!--
|
||||||
|
The test SDK makes this project an executable, so a solution wide
|
||||||
|
`dotnet publish /t:PublishContainer` containerizes it alongside the web app, pushes both
|
||||||
|
to the same repository and tag, and lets the last one to finish win. That shipped an
|
||||||
|
image whose entrypoint was the test assembly, which exits 0 without output, so the
|
||||||
|
container looked like it started and stopped for no reason.
|
||||||
|
The pipelines publish the web app project explicitly; this keeps a solution wide publish
|
||||||
|
from ever producing an image from the tests again.
|
||||||
|
-->
|
||||||
|
<EnableSdkContainerSupport>false</EnableSdkContainerSupport>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -21,17 +31,17 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.12.2" />
|
<PackageReference Include="AwesomeAssertions" Version="9.5.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -41,7 +51,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
<Using Include="FluentAssertions" />
|
<Using Include="AwesomeAssertions" />
|
||||||
<Using Include="Moq" />
|
<Using Include="Moq" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Coverage reports exclude:
|
|||||||
## Test Frameworks and Libraries
|
## Test Frameworks and Libraries
|
||||||
|
|
||||||
- **xUnit**: Primary testing framework
|
- **xUnit**: Primary testing framework
|
||||||
- **FluentAssertions**: For readable assertions
|
- **AwesomeAssertions**: For readable assertions
|
||||||
- **Moq**: For mocking dependencies
|
- **Moq**: For mocking dependencies
|
||||||
- **Coverlet**: For code coverage analysis
|
- **Coverlet**: For code coverage analysis
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ Common test data is provided through the `TestHelper` class:
|
|||||||
2. **Single Responsibility**: Each test should verify one specific behavior
|
2. **Single Responsibility**: Each test should verify one specific behavior
|
||||||
3. **Independence**: Tests should not depend on each other and should be able to run in any order
|
3. **Independence**: Tests should not depend on each other and should be able to run in any order
|
||||||
4. **Cleanup**: Use `IDisposable` or cleanup methods to remove temporary resources
|
4. **Cleanup**: Use `IDisposable` or cleanup methods to remove temporary resources
|
||||||
5. **Readable Assertions**: Use FluentAssertions for more readable test assertions
|
5. **Readable Assertions**: Use AwesomeAssertions for more readable test assertions
|
||||||
6. **Mock Verification**: Verify that mocked methods are called as expected when relevant
|
6. **Mock Verification**: Verify that mocked methods are called as expected when relevant
|
||||||
|
|
||||||
## Continuous Integration
|
## Continuous Integration
|
||||||
|
|||||||
@@ -436,6 +436,37 @@ public class CaddyServiceTests : IDisposable
|
|||||||
File.Exists(filePath).Should().BeTrue();
|
File.Exists(filePath).Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service refuses to save configurations whose file name escapes the configuration directory.
|
||||||
|
/// Setup: Provides save requests with traversal segments and directory separators in the file name.
|
||||||
|
/// Expectation: The service should reject the request and write nothing outside the configuration directory, so an untrusted caller such as the HTTP API cannot write arbitrary files on the host.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[InlineData("../escape")]
|
||||||
|
[InlineData("../../etc/escape")]
|
||||||
|
[InlineData("sub/escape")]
|
||||||
|
[InlineData("sub\\escape")]
|
||||||
|
public void SaveCaddyConfiguration_WithFileNameEscapingConfigDir_ReturnsFailureAndWritesNothing(string fileName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new CaddySaveConfigurationRequest
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Content = TestHelper.SampleCaddyfiles.SimpleReverseProxy,
|
||||||
|
IsNew = true
|
||||||
|
};
|
||||||
|
var escapedPath = Path.GetFullPath(Path.Combine(_tempConfigDir, $"{fileName}.caddy"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.SaveCaddyConfiguration(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Message.Should().Be("The configuration file name contains invalid characters");
|
||||||
|
File.Exists(escapedPath).Should().BeFalse();
|
||||||
|
Directory.GetFiles(_tempConfigDir).Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SaveCaddyGlobalConfiguration Tests
|
#region SaveCaddyGlobalConfiguration Tests
|
||||||
@@ -755,6 +786,37 @@ public class CaddyServiceTests : IDisposable
|
|||||||
result.DeletedConfigurations.Should().BeEmpty();
|
result.DeletedConfigurations.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that the Caddy service refuses to delete configurations whose file name escapes the configuration directory.
|
||||||
|
/// Setup: Creates a file outside the configuration directory and asks the service to delete it through a traversal file name.
|
||||||
|
/// Expectation: The service should report the name as failed and leave the outside file untouched, so an untrusted caller such as the HTTP API cannot delete arbitrary files on the host.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DeleteCaddyConfigurations_WithFileNameEscapingConfigDir_ReportsFailureAndDeletesNothing()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var outsideDir = TestHelper.CreateTempDirectory();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var outsidePath = Path.Combine(outsideDir, "victim.caddy");
|
||||||
|
File.WriteAllText(outsidePath, "content");
|
||||||
|
var traversalName = Path.Combine("..", Path.GetFileName(outsideDir), "victim");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.DeleteCaddyConfigurations([traversalName]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Success.Should().BeFalse();
|
||||||
|
result.Message.Should().Contain(traversalName);
|
||||||
|
result.DeletedConfigurations.Should().BeEmpty();
|
||||||
|
File.Exists(outsidePath).Should().BeTrue();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TestHelper.CleanupDirectory(outsideDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GetCaddyConfigurationInfo Tests
|
#region GetCaddyConfigurationInfo Tests
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<Exclude>[*]Microsoft.*</Exclude>
|
<Exclude>[*]Microsoft.*</Exclude>
|
||||||
<Exclude>[*]System.*</Exclude>
|
<Exclude>[*]System.*</Exclude>
|
||||||
<Exclude>[*]Moq.*</Exclude>
|
<Exclude>[*]Moq.*</Exclude>
|
||||||
<Exclude>[*]FluentAssertions.*</Exclude>
|
<Exclude>[*]AwesomeAssertions.*</Exclude>
|
||||||
<Exclude>[*]xunit.*</Exclude>
|
<Exclude>[*]xunit.*</Exclude>
|
||||||
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
|
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
|
||||||
<SingleHit>false</SingleHit>
|
<SingleHit>false</SingleHit>
|
||||||
|
|||||||
252
CaddyManager/Api/CaddyApi.cs
Normal file
252
CaddyManager/Api/CaddyApi.cs
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using CaddyManager.Contracts.Caddy;
|
||||||
|
using CaddyManager.Contracts.Docker;
|
||||||
|
using CaddyManager.Contracts.Models.Caddy;
|
||||||
|
|
||||||
|
namespace CaddyManager.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP surface over the same services the Blazor UI uses, so scripts can manage reverse proxy
|
||||||
|
/// configurations and reload Caddy without a browser
|
||||||
|
/// </summary>
|
||||||
|
public static class CaddyApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Header carrying the shared API key
|
||||||
|
/// </summary>
|
||||||
|
public const string ApiKeyHeader = "X-Api-Key";
|
||||||
|
|
||||||
|
private const string ReverseProxiesTag = "Reverse proxies";
|
||||||
|
private const string CaddyTag = "Caddy";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps every /api endpoint behind the shared key check
|
||||||
|
/// </summary>
|
||||||
|
public static void MapCaddyApi(this WebApplication app)
|
||||||
|
{
|
||||||
|
// Read once at startup; the key is a single value and does not warrant a configuration class
|
||||||
|
var apiKey = app.Configuration["Api:Key"];
|
||||||
|
|
||||||
|
var api = app.MapGroup("/api")
|
||||||
|
// Antiforgery only validates form content types, but the API is JSON only and should
|
||||||
|
// never start failing because a caller switched to form encoding
|
||||||
|
.DisableAntiforgery()
|
||||||
|
.AddEndpointFilter(async (context, next) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
// Fail closed: this API writes reverse proxy configuration and can restart Caddy
|
||||||
|
return Results.Problem("API disabled: Api:Key is not configured",
|
||||||
|
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
var provided = context.HttpContext.Request.Headers[ApiKeyHeader].ToString();
|
||||||
|
if (!CryptographicOperations.FixedTimeEquals(
|
||||||
|
Encoding.UTF8.GetBytes(provided), Encoding.UTF8.GetBytes(apiKey)))
|
||||||
|
{
|
||||||
|
return Results.Problem($"Invalid or missing {ApiKeyHeader} header",
|
||||||
|
statusCode: StatusCodes.Status401Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
MapConfigurationEndpoints(api);
|
||||||
|
MapCaddyEndpoints(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapConfigurationEndpoints(RouteGroupBuilder api)
|
||||||
|
{
|
||||||
|
var group = api.MapGroup("/configurations").WithTags(ReverseProxiesTag);
|
||||||
|
|
||||||
|
group.MapGet("", (ICaddyService caddyService) => caddyService.GetExistingCaddyConfigurations())
|
||||||
|
.WithSummary("List reverse proxy configurations")
|
||||||
|
.WithDescription("Returns every *.caddy file in the configuration directory, parsed for hostnames, upstream target, ports and tags. The global Caddyfile is not included.");
|
||||||
|
|
||||||
|
group.MapGet("/{name}", IResult (string name, ICaddyService caddyService) =>
|
||||||
|
{
|
||||||
|
if (!Exists(caddyService, name))
|
||||||
|
{
|
||||||
|
return Results.NotFound(Failure($"The configuration {name} does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(new ConfigurationResponse(
|
||||||
|
caddyService.GetCaddyConfigurationInfo(name),
|
||||||
|
caddyService.GetCaddyConfigurationContent(name)));
|
||||||
|
})
|
||||||
|
.WithSummary("Get a reverse proxy configuration")
|
||||||
|
.Produces<ConfigurationResponse>()
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPost("", IResult (CreateConfigurationRequest request, ICaddyService caddyService) =>
|
||||||
|
{
|
||||||
|
var response = caddyService.SaveCaddyConfiguration(new CaddySaveConfigurationRequest
|
||||||
|
{
|
||||||
|
IsNew = true,
|
||||||
|
FileName = request.FileName,
|
||||||
|
Content = request.Content,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.Success
|
||||||
|
? Results.Created($"/api/configurations/{request.FileName}", response)
|
||||||
|
: ToResult(response);
|
||||||
|
})
|
||||||
|
.WithSummary("Create a reverse proxy configuration")
|
||||||
|
.WithDescription("Fails if a configuration with the same file name already exists; use PUT to overwrite one.")
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status201Created)
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
|
group.MapPut("/{name}", IResult (string name, SaveContentRequest request, ICaddyService caddyService) =>
|
||||||
|
{
|
||||||
|
if (!Exists(caddyService, name))
|
||||||
|
{
|
||||||
|
return Results.NotFound(Failure($"The configuration {name} does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ToResult(caddyService.SaveCaddyConfiguration(new CaddySaveConfigurationRequest
|
||||||
|
{
|
||||||
|
IsNew = false,
|
||||||
|
FileName = name,
|
||||||
|
Content = request.Content,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.WithSummary("Replace the content of a reverse proxy configuration")
|
||||||
|
.Produces<CaddyOperationResponse>()
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
group.MapPost("/{name}/rename",
|
||||||
|
IResult (string name, RenameConfigurationRequest request, ICaddyService caddyService) =>
|
||||||
|
ToResult(caddyService.RenameCaddyConfiguration(name, request.NewFileName)))
|
||||||
|
.WithSummary("Rename a reverse proxy configuration")
|
||||||
|
.WithDescription("Renames the file only. An import of the old name in the global Caddyfile is left untouched and has to be updated separately.")
|
||||||
|
.Produces<CaddyOperationResponse>()
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status404NotFound)
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status409Conflict);
|
||||||
|
|
||||||
|
group.MapDelete("/{name}", IResult (string name, ICaddyService caddyService) =>
|
||||||
|
{
|
||||||
|
var response = caddyService.DeleteCaddyConfigurations([name]);
|
||||||
|
return response.Success
|
||||||
|
? Results.Ok(response)
|
||||||
|
: Results.NotFound(response);
|
||||||
|
})
|
||||||
|
.WithSummary("Delete a reverse proxy configuration")
|
||||||
|
.Produces<CaddyDeleteOperationResponse>()
|
||||||
|
.Produces<CaddyDeleteOperationResponse>(StatusCodes.Status404NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MapCaddyEndpoints(RouteGroupBuilder api)
|
||||||
|
{
|
||||||
|
// Empty prefix so the tag stays scoped to these endpoints instead of leaking onto the whole /api group
|
||||||
|
var group = api.MapGroup("").WithTags(CaddyTag);
|
||||||
|
|
||||||
|
group.MapGet("/caddyfile",
|
||||||
|
(ICaddyService caddyService) => new ContentResponse(caddyService.GetCaddyGlobalConfigurationContent()))
|
||||||
|
.WithSummary("Get the global Caddyfile")
|
||||||
|
.WithDescription("The global Caddyfile is the entry point Caddy loads; it usually imports the individual *.caddy configurations.");
|
||||||
|
|
||||||
|
group.MapPut("/caddyfile",
|
||||||
|
IResult (SaveContentRequest request, ICaddyService caddyService) =>
|
||||||
|
ToResult(caddyService.SaveCaddyGlobalConfiguration(request.Content)))
|
||||||
|
.WithSummary("Replace the global Caddyfile")
|
||||||
|
.Produces<CaddyOperationResponse>()
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
group.MapPost("/caddy/reload", async Task<IResult> (IDockerService dockerService) =>
|
||||||
|
{
|
||||||
|
var response = await dockerService.ReloadCaddyContainerAsync();
|
||||||
|
return response.Success
|
||||||
|
? Results.Ok(response)
|
||||||
|
// The reload itself failed (bad config, container down); the request was fine
|
||||||
|
: Results.Json(response, statusCode: StatusCodes.Status502BadGateway);
|
||||||
|
})
|
||||||
|
.WithSummary("Reload the Caddy configuration")
|
||||||
|
.WithDescription("Runs `caddy reload` inside the Caddy container, which applies configuration changes without dropping connections.")
|
||||||
|
.Produces<CaddyReloadResponse>()
|
||||||
|
.Produces<CaddyReloadResponse>(StatusCodes.Status502BadGateway);
|
||||||
|
|
||||||
|
group.MapPost("/caddy/restart", async Task<IResult> (IDockerService dockerService) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await dockerService.RestartCaddyContainerAsync();
|
||||||
|
return Results.Accepted();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// Restart reports problems by throwing rather than by a response object, the way
|
||||||
|
// reload does, so it needs the same try/catch the UI puts around it
|
||||||
|
return Results.Json(Failure(e.Message), statusCode: StatusCodes.Status502BadGateway);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.WithSummary("Restart the Caddy container")
|
||||||
|
.WithDescription("Returns 202 once the restart has been requested. A missing Caddy container is not reported as an error, matching the UI behaviour.")
|
||||||
|
.Produces(StatusCodes.Status202Accepted)
|
||||||
|
.Produces<CaddyOperationResponse>(StatusCodes.Status502BadGateway);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The global Caddyfile is excluded from the listing, and reading a missing file yields an empty
|
||||||
|
/// string, so the listing is the only way to tell missing from empty
|
||||||
|
/// </summary>
|
||||||
|
private static bool Exists(ICaddyService caddyService, string name) =>
|
||||||
|
caddyService.GetExistingCaddyConfigurations().Any(configuration => configuration.FileName == name);
|
||||||
|
|
||||||
|
private static CaddyOperationResponse Failure(string message) => new()
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = message,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Turns a service response into a status code. The service reports failures as messages rather
|
||||||
|
/// than typed errors, so the known ones are matched here and anything else is treated as a fault
|
||||||
|
/// </summary>
|
||||||
|
private static IResult ToResult(CaddyOperationResponse response) => response.Success
|
||||||
|
? Results.Ok(response)
|
||||||
|
: Results.Json(response, statusCode: response.Message switch
|
||||||
|
{
|
||||||
|
"The configuration file already exists" => StatusCodes.Status409Conflict,
|
||||||
|
"The configuration file to rename does not exist" => StatusCodes.Status404NotFound,
|
||||||
|
"The global Caddyfile cannot be renamed" => StatusCodes.Status400BadRequest,
|
||||||
|
var message when message.StartsWith("The configuration file name") =>
|
||||||
|
StatusCodes.Status400BadRequest,
|
||||||
|
_ => StatusCodes.Status500InternalServerError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A reverse proxy configuration together with its raw Caddyfile content
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Info">Parsed summary of the configuration</param>
|
||||||
|
/// <param name="Content">Raw content of the .caddy file</param>
|
||||||
|
public record ConfigurationResponse(CaddyConfigurationInfo Info, string Content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raw content of a configuration file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Content">Raw Caddyfile content</param>
|
||||||
|
public record ContentResponse(string Content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to create a new reverse proxy configuration
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="FileName">File name without the .caddy extension</param>
|
||||||
|
/// <param name="Content">Raw Caddyfile content</param>
|
||||||
|
public record CreateConfigurationRequest(string FileName, string Content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to replace the content of an existing configuration
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Content">Raw Caddyfile content</param>
|
||||||
|
public record SaveContentRequest(string Content);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to rename a configuration file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="NewFileName">New file name without the .caddy extension</param>
|
||||||
|
public record RenameConfigurationRequest(string NewFileName);
|
||||||
@@ -1,17 +1,38 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<LangVersion>13</LangVersion>
|
<LangVersion>14</LangVersion>
|
||||||
<ContainerRepository>caddy-manager</ContainerRepository>
|
<ContainerRepository>caddy-manager</ContainerRepository>
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<!-- To avoid error on lock file not found -->
|
<!-- To avoid error on lock file not found -->
|
||||||
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
|
||||||
|
<!--
|
||||||
|
Low-traffic single-user admin UI: trade GC throughput for a much smaller heap.
|
||||||
|
Server GC (the Web SDK default) reserves one heap per CPU core.
|
||||||
|
-->
|
||||||
|
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||||
|
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
|
||||||
|
<TieredPGO>false</TieredPGO>
|
||||||
|
<!-- No localized content, so skip loading ICU entirely (alpine ships no ICU anyway) -->
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0-alpine</ContainerBaseImage>
|
||||||
|
<!--
|
||||||
|
The alpine base is musl. Declared here so a plain `dotnet restore` evaluates the
|
||||||
|
same RID the container publish uses, keeping packages.lock.json consistent for
|
||||||
|
CI's locked-mode restore (otherwise NU1004).
|
||||||
|
-->
|
||||||
|
<RuntimeIdentifiers>linux-musl-x64</RuntimeIdentifiers>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- No MSBuild property exists for this one; lands in runtimeconfig.json for both build paths -->
|
||||||
|
<RuntimeHostConfigurationOption Include="System.GC.ConserveMemory" Value="9" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\.dockerignore">
|
<Content Include="..\.dockerignore">
|
||||||
<Link>.dockerignore</Link>
|
<Link>.dockerignore</Link>
|
||||||
@@ -19,11 +40,15 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BlazorMonaco" Version="3.3.0" />
|
<PackageReference Include="BlazorMonaco" Version="3.5.0" />
|
||||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||||
<PackageReference Include="Humanizer" Version="3.0.0-beta.96" />
|
<PackageReference Include="Humanizer" Version="3.0.10" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||||
|
<!-- Overrides the 2.0.0 that Microsoft.AspNetCore.OpenApi pulls in, which has advisory GHSA-v5pm-xwqc-g5wc -->
|
||||||
|
<PackageReference Include="Microsoft.OpenApi" Version="2.11.0" />
|
||||||
|
<PackageReference Include="MudBlazor" Version="9.7.0" />
|
||||||
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.2.1" />
|
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.2.1" />
|
||||||
|
<PackageReference Include="Scalar.AspNetCore" Version="2.16.16" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ public partial class MainLayout
|
|||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
// Get the system preference for dark mode
|
// Get the system preference for dark mode
|
||||||
_isDarkMode = await _mudThemeProvider.GetSystemPreference();
|
_isDarkMode = await _mudThemeProvider.GetSystemDarkModeAsync();
|
||||||
await _mudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
|
await _mudThemeProvider.WatchSystemDarkModeAsync(OnSystemPreferenceChanged);
|
||||||
_isInitialing = false;
|
_isInitialing = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
@bind-SelectedValues="_selectedCaddyConfigurations">
|
@bind-SelectedValues="_selectedCaddyConfigurations">
|
||||||
@foreach (var (index, caddyConfig) in _availableCaddyConfigurations.Index())
|
@foreach (var (index, caddyConfig) in _availableCaddyConfigurations.Index())
|
||||||
{
|
{
|
||||||
<CaddyReverseProxyItem ConfigurationInfo="@caddyConfig" OnCaddyRestartRequired="@ReloadCaddy"
|
<CaddyReverseProxyItem ConfigurationInfo="@caddyConfig"
|
||||||
OnCaddyfileDuplicateRequested="@HandleDuplicateRequest" />
|
OnEditRequested="@(fileName => ShowCaddyfileEditorDialog(fileName))" />
|
||||||
|
|
||||||
@if (index < _availableCaddyConfigurations.Count - 1)
|
@if (index < _availableCaddyConfigurations.Count - 1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task ShowCaddyfileEditorDialog(string fileName, string initialContent = "")
|
private async Task ShowCaddyfileEditorDialog(string fileName, string initialContent = "")
|
||||||
{
|
{
|
||||||
var dialog = await DialogService.ShowAsync<CaddyfileEditorComponent>("New configuration",
|
var dialog = await DialogService.ShowAsync<CaddyfileEditorComponent>(
|
||||||
|
string.IsNullOrWhiteSpace(fileName) ? "New configuration" : "Caddy file",
|
||||||
options: new DialogOptions
|
options: new DialogOptions
|
||||||
{
|
{
|
||||||
FullWidth = true,
|
FullWidth = true,
|
||||||
@@ -71,7 +72,8 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
|||||||
}, parameters: new DialogParameters<CaddyfileEditorComponent>
|
}, parameters: new DialogParameters<CaddyfileEditorComponent>
|
||||||
{
|
{
|
||||||
{ p => p.FileName, fileName },
|
{ p => p.FileName, fileName },
|
||||||
{ p => p.InitialContent, initialContent }
|
{ p => p.InitialContent, initialContent },
|
||||||
|
{ p => p.OnDuplicate, EventCallback.Factory.Create<string>(this, HandleDuplicateRequest) }
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
@@ -81,6 +83,7 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
|||||||
await ReloadCaddy();
|
await ReloadCaddy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always rebuild the list, the configuration may have been renamed or had its hostnames and ports changed
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using CaddyManager.Contracts.Caddy;
|
|
||||||
using CaddyManager.Contracts.Models.Caddy;
|
using CaddyManager.Contracts.Models.Caddy;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
|
||||||
|
|
||||||
namespace CaddyManager.Components.Pages.Caddy.CaddyReverseProxies;
|
namespace CaddyManager.Components.Pages.Caddy.CaddyReverseProxies;
|
||||||
|
|
||||||
@@ -12,52 +10,18 @@ namespace CaddyManager.Components.Pages.Caddy.CaddyReverseProxies;
|
|||||||
public partial class CaddyReverseProxyItem : ComponentBase
|
public partial class CaddyReverseProxyItem : ComponentBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Callback to refresh the Caddy reverse proxies on the main page
|
/// Callback asking the main page to open the editor for this configuration. The page owns the dialog so that it
|
||||||
|
/// can refresh the whole list afterwards, which matters when the configuration is renamed or deleted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback OnCaddyRestartRequired { get; set; }
|
public EventCallback<string> OnEditRequested { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public EventCallback<string> OnCaddyfileDuplicateRequested { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public CaddyConfigurationInfo ConfigurationInfo { get; set; } = null!;
|
public CaddyConfigurationInfo ConfigurationInfo { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dialog service for showing the Caddy file editor dialog
|
/// Request the Caddy file editor dialog for this configuration
|
||||||
/// </summary>
|
|
||||||
[Inject]
|
|
||||||
private IDialogService DialogService { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Caddy service for ops on the Caddy configuration
|
|
||||||
/// </summary>
|
|
||||||
[Inject] private ICaddyService CaddyService { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show the Caddy file editor dialog
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private async Task Edit()
|
private Task Edit() => OnEditRequested.InvokeAsync(ConfigurationInfo.FileName);
|
||||||
{
|
|
||||||
var dialog = await DialogService.ShowAsync<CaddyfileEditor.CaddyfileEditor>("Caddy file", options: new DialogOptions
|
|
||||||
{
|
|
||||||
FullWidth = true,
|
|
||||||
MaxWidth = MaxWidth.Medium,
|
|
||||||
}, parameters: new DialogParameters<CaddyfileEditor.CaddyfileEditor>
|
|
||||||
{
|
|
||||||
{ p => p.FileName, ConfigurationInfo.FileName },
|
|
||||||
{ p => p.OnDuplicate, EventCallback.Factory.Create(this, OnCaddyfileDuplicateRequested) }
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await dialog.Result;
|
|
||||||
|
|
||||||
ConfigurationInfo = CaddyService.GetCaddyConfigurationInfo(ConfigurationInfo.FileName);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
if (result is { Data: bool, Canceled: false } && (bool)result.Data)
|
|
||||||
{
|
|
||||||
await OnCaddyRestartRequired.InvokeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,6 @@
|
|||||||
<MudButton OnClick="Duplicate">Duplicate</MudButton>
|
<MudButton OnClick="Duplicate">Duplicate</MudButton>
|
||||||
}
|
}
|
||||||
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
|
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
|
||||||
<MudButton Color="Color.Secondary" OnClick="SaveAndRestart">Save & Restart</MudButton>
|
<MudButton Color="Color.Secondary" OnClick="SaveAndReload">Save & Reload</MudButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
@@ -144,13 +144,13 @@ public partial class CaddyfileEditor : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the Caddy configuration file and restarts the Caddy container
|
/// Saves the Caddy configuration file and reloads the Caddy configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SaveAndRestart()
|
private async Task SaveAndReload()
|
||||||
{
|
{
|
||||||
if (await Save())
|
if (await Save())
|
||||||
{
|
{
|
||||||
// Indicate successful save and that a restart is required by the calling component
|
// Indicate successful save and that a reload is required by the calling component
|
||||||
MudDialog.Close(DialogResult.Ok(true));
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public class ApplicationInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The version of the application, to be defined and tagged
|
/// The version of the application, to be defined and tagged
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly string Version = "1.1.0";
|
public static readonly string Version = "1.2.2";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The commit hash of the application
|
/// The commit hash of the application
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
|
||||||
USER $APP_UID
|
USER $APP_UID
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["CaddyManager/CaddyManager.csproj", "CaddyManager/"]
|
COPY ["CaddyManager/CaddyManager.csproj", "CaddyManager/packages.lock.json", "CaddyManager/"]
|
||||||
|
COPY ["CaddyManager.Contracts/CaddyManager.Contracts.csproj", "CaddyManager.Contracts/"]
|
||||||
|
COPY ["CaddyManager.Services/CaddyManager.Services.csproj", "CaddyManager.Services/"]
|
||||||
RUN dotnet restore "CaddyManager/CaddyManager.csproj"
|
RUN dotnet restore "CaddyManager/CaddyManager.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/CaddyManager"
|
RUN dotnet publish "CaddyManager/CaddyManager.csproj" -c $BUILD_CONFIGURATION -o /app/publish --no-restore /p:UseAppHost=false
|
||||||
RUN dotnet build "CaddyManager.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
|
||||||
|
|
||||||
FROM build AS publish
|
|
||||||
ARG BUILD_CONFIGURATION=Release
|
|
||||||
RUN dotnet publish "CaddyManager.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
|
||||||
|
|
||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=build /app/publish .
|
||||||
ENTRYPOINT ["dotnet", "CaddyManager.dll"]
|
ENTRYPOINT ["dotnet", "CaddyManager.dll"]
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
using CaddyManager.Api;
|
||||||
using CaddyManager.Components;
|
using CaddyManager.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.OpenApi;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using NetCore.AutoRegisterDi;
|
using NetCore.AutoRegisterDi;
|
||||||
|
using Scalar.AspNetCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddMudServices()
|
|
||||||
.AddRazorComponents()
|
.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
@@ -15,7 +19,56 @@ builder.Services.RegisterAssemblyPublicNonGenericClasses(System.Reflection.Assem
|
|||||||
.Where(t => t.Name.EndsWith("Service"))
|
.Where(t => t.Name.EndsWith("Service"))
|
||||||
.AsPublicImplementedInterfaces();
|
.AsPublicImplementedInterfaces();
|
||||||
|
|
||||||
builder.Services.AddSignalR(e => { e.MaximumReceiveMessageSize = 102400000; });
|
builder.Services.AddSignalR(e =>
|
||||||
|
{
|
||||||
|
// Caddyfiles are kilobytes; this is generous headroom for Monaco editor round-trips
|
||||||
|
e.MaximumReceiveMessageSize = 512 * 1024;
|
||||||
|
e.StreamBufferCapacity = 5;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep as little per-circuit state resident as practical for a single-user admin UI
|
||||||
|
builder.Services.Configure<CircuitOptions>(o =>
|
||||||
|
{
|
||||||
|
o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(1);
|
||||||
|
o.DisconnectedCircuitMaxRetained = 5;
|
||||||
|
o.MaxBufferedUnacknowledgedRenderBatches = 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddOpenApi(options =>
|
||||||
|
{
|
||||||
|
// Declare the shared key scheme so the docs page offers an auth box and marks every endpoint as secured
|
||||||
|
options.AddDocumentTransformer((document, _, _) =>
|
||||||
|
{
|
||||||
|
document.Components ??= new OpenApiComponents();
|
||||||
|
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
||||||
|
document.Components.SecuritySchemes[CaddyApi.ApiKeyHeader] = new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Name = CaddyApi.ApiKeyHeader,
|
||||||
|
Description = "Shared key configured through Api:Key (environment variable Api__Key)",
|
||||||
|
};
|
||||||
|
document.Security ??= [];
|
||||||
|
document.Security.Add(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
[new OpenApiSecuritySchemeReference(CaddyApi.ApiKeyHeader, document)] = [],
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Caddy terminates TLS and forwards plain HTTP, so without this the app thinks every request is
|
||||||
|
// http and the OpenAPI document advertises http:// server URLs, which the browser blocks as mixed
|
||||||
|
// content when the docs page itself was served over https
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto |
|
||||||
|
ForwardedHeaders.XForwardedHost;
|
||||||
|
// The proxy is another container on a Docker network, so its address is not known up front;
|
||||||
|
// this app is only ever meant to be reached through that proxy
|
||||||
|
options.KnownIPNetworks.Clear();
|
||||||
|
options.KnownProxies.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
@@ -26,6 +79,9 @@ builder.Services.AddMudServices(config =>
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Has to run before anything that reads the scheme, host or client address
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -43,4 +99,8 @@ app.MapStaticAssets();
|
|||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
app.MapCaddyApi();
|
||||||
|
app.MapOpenApi();
|
||||||
|
app.MapScalarApiReference();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
"Api": {
|
||||||
|
"Key": ""
|
||||||
|
},
|
||||||
"CaddyService": {
|
"CaddyService": {
|
||||||
"ConfigDir": "/root/compose/caddy/config"
|
"ConfigDir": "/root/compose/caddy/config"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net9.0": {
|
"net10.0": {
|
||||||
"BlazorMonaco": {
|
"BlazorMonaco": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.3.0, )",
|
"requested": "[3.5.0, )",
|
||||||
"resolved": "3.3.0",
|
"resolved": "3.5.0",
|
||||||
"contentHash": "ywtUCZMfmaNBadhQbZEGPerdptHUcyVbPB2Ug1HTNzarJlMCJFSBRvQZefS383h/vDhFgHUrR7/pE6scIc6lJg==",
|
"contentHash": "MfEzmLlhGEUCi4O65jnDipDUf0UZz3sO9tWupp09hMSvHWcYIP7NRG6r3PIfBUz78nfEkU+aW7Jyg71gd8QcPQ=="
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Components": "9.0.0",
|
|
||||||
"Microsoft.AspNetCore.Components.Web": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Docker.DotNet": {
|
"Docker.DotNet": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
@@ -18,694 +14,525 @@
|
|||||||
"resolved": "3.125.15",
|
"resolved": "3.125.15",
|
||||||
"contentHash": "XN8FKxVv8Mjmwu104/Hl9lM61pLY675s70gzwSj8KR5pwblo8HfWLcCuinh9kYsqujBkMH4HVRCEcRuU6al4BQ==",
|
"contentHash": "XN8FKxVv8Mjmwu104/Hl9lM61pLY675s70gzwSj8KR5pwblo8HfWLcCuinh9kYsqujBkMH4HVRCEcRuU6al4BQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Newtonsoft.Json": "13.0.1",
|
"Newtonsoft.Json": "13.0.1"
|
||||||
"System.Buffers": "4.5.1",
|
|
||||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer": {
|
"Humanizer": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.0.0-beta.96, )",
|
"requested": "[3.0.10, )",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "T1X21b+0l3jYDH1DztroJIeXdCwlNGmNYDidru9TzcLahwceQ48UGMjOQeWSa1v8zncPvhJzLzVAYWmOZcOySA==",
|
"contentHash": "13+6RiWdpVCzSn2SSEd1hAIr/UHScNdLCUhRgjvI60UcD5u3vXu3u1/Z+74OcHWum1itjiBTjYXgJKQna1jTgw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core.af": "3.0.0-beta.96",
|
"Humanizer.Core.af": "3.0.10",
|
||||||
"Humanizer.Core.ar": "3.0.0-beta.96",
|
"Humanizer.Core.ar": "3.0.10",
|
||||||
"Humanizer.Core.az": "3.0.0-beta.96",
|
"Humanizer.Core.az": "3.0.10",
|
||||||
"Humanizer.Core.bg": "3.0.0-beta.96",
|
"Humanizer.Core.bg": "3.0.10",
|
||||||
"Humanizer.Core.bn-BD": "3.0.0-beta.96",
|
"Humanizer.Core.bn": "3.0.10",
|
||||||
"Humanizer.Core.cs": "3.0.0-beta.96",
|
"Humanizer.Core.ca": "3.0.10",
|
||||||
"Humanizer.Core.da": "3.0.0-beta.96",
|
"Humanizer.Core.cs": "3.0.10",
|
||||||
"Humanizer.Core.de": "3.0.0-beta.96",
|
"Humanizer.Core.da": "3.0.10",
|
||||||
"Humanizer.Core.el": "3.0.0-beta.96",
|
"Humanizer.Core.de": "3.0.10",
|
||||||
"Humanizer.Core.es": "3.0.0-beta.96",
|
"Humanizer.Core.el": "3.0.10",
|
||||||
"Humanizer.Core.fa": "3.0.0-beta.96",
|
"Humanizer.Core.es": "3.0.10",
|
||||||
"Humanizer.Core.fi-FI": "3.0.0-beta.96",
|
"Humanizer.Core.fa": "3.0.10",
|
||||||
"Humanizer.Core.fr": "3.0.0-beta.96",
|
"Humanizer.Core.fi": "3.0.10",
|
||||||
"Humanizer.Core.fr-BE": "3.0.0-beta.96",
|
"Humanizer.Core.fil": "3.0.10",
|
||||||
"Humanizer.Core.he": "3.0.0-beta.96",
|
"Humanizer.Core.fr": "3.0.10",
|
||||||
"Humanizer.Core.hr": "3.0.0-beta.96",
|
"Humanizer.Core.he": "3.0.10",
|
||||||
"Humanizer.Core.hu": "3.0.0-beta.96",
|
"Humanizer.Core.hr": "3.0.10",
|
||||||
"Humanizer.Core.hy": "3.0.0-beta.96",
|
"Humanizer.Core.hu": "3.0.10",
|
||||||
"Humanizer.Core.id": "3.0.0-beta.96",
|
"Humanizer.Core.hy": "3.0.10",
|
||||||
"Humanizer.Core.is": "3.0.0-beta.96",
|
"Humanizer.Core.id": "3.0.10",
|
||||||
"Humanizer.Core.it": "3.0.0-beta.96",
|
"Humanizer.Core.is": "3.0.10",
|
||||||
"Humanizer.Core.ja": "3.0.0-beta.96",
|
"Humanizer.Core.it": "3.0.10",
|
||||||
"Humanizer.Core.ko-KR": "3.0.0-beta.96",
|
"Humanizer.Core.ja": "3.0.10",
|
||||||
"Humanizer.Core.ku": "3.0.0-beta.96",
|
"Humanizer.Core.ko": "3.0.10",
|
||||||
"Humanizer.Core.lb": "3.0.0-beta.96",
|
"Humanizer.Core.ku": "3.0.10",
|
||||||
"Humanizer.Core.lt": "3.0.0-beta.96",
|
"Humanizer.Core.lb": "3.0.10",
|
||||||
"Humanizer.Core.lv": "3.0.0-beta.96",
|
"Humanizer.Core.lt": "3.0.10",
|
||||||
"Humanizer.Core.ms-MY": "3.0.0-beta.96",
|
"Humanizer.Core.lv": "3.0.10",
|
||||||
"Humanizer.Core.mt": "3.0.0-beta.96",
|
"Humanizer.Core.ms": "3.0.10",
|
||||||
"Humanizer.Core.nb": "3.0.0-beta.96",
|
"Humanizer.Core.mt": "3.0.10",
|
||||||
"Humanizer.Core.nb-NO": "3.0.0-beta.96",
|
"Humanizer.Core.nb": "3.0.10",
|
||||||
"Humanizer.Core.nl": "3.0.0-beta.96",
|
"Humanizer.Core.nl": "3.0.10",
|
||||||
"Humanizer.Core.pl": "3.0.0-beta.96",
|
"Humanizer.Core.pl": "3.0.10",
|
||||||
"Humanizer.Core.pt": "3.0.0-beta.96",
|
"Humanizer.Core.pt": "3.0.10",
|
||||||
"Humanizer.Core.ro": "3.0.0-beta.96",
|
"Humanizer.Core.pt-BR": "3.0.10",
|
||||||
"Humanizer.Core.ru": "3.0.0-beta.96",
|
"Humanizer.Core.ro": "3.0.10",
|
||||||
"Humanizer.Core.sk": "3.0.0-beta.96",
|
"Humanizer.Core.ru": "3.0.10",
|
||||||
"Humanizer.Core.sl": "3.0.0-beta.96",
|
"Humanizer.Core.sk": "3.0.10",
|
||||||
"Humanizer.Core.sr": "3.0.0-beta.96",
|
"Humanizer.Core.sl": "3.0.10",
|
||||||
"Humanizer.Core.sr-Latn": "3.0.0-beta.96",
|
"Humanizer.Core.sr": "3.0.10",
|
||||||
"Humanizer.Core.sv": "3.0.0-beta.96",
|
"Humanizer.Core.sr-Latn": "3.0.10",
|
||||||
"Humanizer.Core.th-TH": "3.0.0-beta.96",
|
"Humanizer.Core.sv": "3.0.10",
|
||||||
"Humanizer.Core.tr": "3.0.0-beta.96",
|
"Humanizer.Core.th": "3.0.10",
|
||||||
"Humanizer.Core.uk": "3.0.0-beta.96",
|
"Humanizer.Core.tr": "3.0.10",
|
||||||
"Humanizer.Core.uz-Cyrl-UZ": "3.0.0-beta.96",
|
"Humanizer.Core.uk": "3.0.10",
|
||||||
"Humanizer.Core.uz-Latn-UZ": "3.0.0-beta.96",
|
"Humanizer.Core.uz-Cyrl-UZ": "3.0.10",
|
||||||
"Humanizer.Core.vi": "3.0.0-beta.96",
|
"Humanizer.Core.uz-Latn-UZ": "3.0.10",
|
||||||
"Humanizer.Core.zh-CN": "3.0.0-beta.96",
|
"Humanizer.Core.vi": "3.0.10",
|
||||||
"Humanizer.Core.zh-Hans": "3.0.0-beta.96",
|
"Humanizer.Core.zh-CN": "3.0.10",
|
||||||
"Humanizer.Core.zh-Hant": "3.0.0-beta.96"
|
"Humanizer.Core.zh-Hans": "3.0.10",
|
||||||
|
"Humanizer.Core.zh-Hant": "3.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.AspNetCore.App.Internal.Assets": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.10, )",
|
||||||
|
"resolved": "10.0.10",
|
||||||
|
"contentHash": "8k0KoYKvNrSqP8E3oDJMBVKavlTEE6BacdLGtEDcxv9Hz0XxWFkD6JdLJsxXUlGeAfZ6YfvWmxKenl73qeCTSA=="
|
||||||
|
},
|
||||||
|
"Microsoft.AspNetCore.OpenApi": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.10, )",
|
||||||
|
"resolved": "10.0.10",
|
||||||
|
"contentHash": "d4Atx9IHq7JgX0F/h7Db+m9zAUzC+cKdI9k+OWnnyQIOUQtfvjIEuhvbjPigVMkAmPUgCbJ8Yp6M9ghUqHtJSQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.OpenApi": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.OpenApi": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.11.0, )",
|
||||||
|
"resolved": "2.11.0",
|
||||||
|
"contentHash": "/ignjfdeKT2SGLIR7QEv19KnI0rvoxRG/TYDOZdK9EsWLjKK9IK8i1Mo5NRm9PRV3i64DzlTqnIflWvoyfljLg=="
|
||||||
|
},
|
||||||
"MudBlazor": {
|
"MudBlazor": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[8.0.0, )",
|
"requested": "[9.7.0, )",
|
||||||
"resolved": "8.0.0",
|
"resolved": "9.7.0",
|
||||||
"contentHash": "itY10dugofdvLYnJ1VZSzUIiKVqNvnTz4eEMYlHUdrOfsk9xShj7k1EW5H6ssMmkbQzShlLBHlUt+VPFGoEJGw==",
|
"contentHash": "uWOgNn9B556J/TL8KFoSqpA3YD2heqOpK5etcofwlj1RiEX1IDy0NmwYE9urlTUQ8zProEK5GqwTBnoObFPN3g=="
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Components": "9.0.1",
|
|
||||||
"Microsoft.AspNetCore.Components.Web": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Localization": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"NetCore.AutoRegisterDi": {
|
"NetCore.AutoRegisterDi": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.2.1, )",
|
"requested": "[2.2.1, )",
|
||||||
"resolved": "2.2.1",
|
"resolved": "2.2.1",
|
||||||
"contentHash": "qRda/VP+Lxak/GCGfT3PqXE6VA+bCbf2wlExcUWwnkwY1d6cfWv4Fp5RRN6dChlFhI8tbmzlNutYwxlA8kBb1A==",
|
"contentHash": "qRda/VP+Lxak/GCGfT3PqXE6VA+bCbf2wlExcUWwnkwY1d6cfWv4Fp5RRN6dChlFhI8tbmzlNutYwxlA8kBb1A=="
|
||||||
"dependencies": {
|
},
|
||||||
"Microsoft.Extensions.DependencyInjection": "2.1.1"
|
"Scalar.AspNetCore": {
|
||||||
}
|
"type": "Direct",
|
||||||
|
"requested": "[2.16.16, )",
|
||||||
|
"resolved": "2.16.16",
|
||||||
|
"contentHash": "Ax0e0bIh+Upf92k1+pTBUom3e/kbpu20qsrDYmmS1NM721Eq2xF8c789x6IbiNrvW8WwySRzPv/icYYhb6idSg=="
|
||||||
},
|
},
|
||||||
"Humanizer.Core": {
|
"Humanizer.Core": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "anMn7QrFRWthyt08MSDFdUKoOwMqNqDpI9AJ3YIgJZJq1qmJd9sjHiKgJM/Ov0WyLEE8pARzyxOkIbXX1pBd0w=="
|
"contentHash": "yZIhtw8sYuvsONzQbZxWpR60tMWYHXoo0DL6nyOqSFiU5POjBTSEyWFpTQtJEZuy+oqiYTXKXY/Mjx7KnqIQFw=="
|
||||||
},
|
},
|
||||||
"Humanizer.Core.af": {
|
"Humanizer.Core.af": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "8/9iF3PY2Dm5M87AyU02LoUZV634znBVQsuMQu+yjxtAgGYAksL0skikU1IblDEp2Hh7I2ejOa48Jb/MjIvSEw==",
|
"contentHash": "jIpC73aOXfhqGEnpFKgO2AJ2Esb2t40c/cKqR+RgK1yg1bcBZBuhtjs+PrAIHf5Lp7Znd4teLjCGDS5ut8O1cw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ar": {
|
"Humanizer.Core.ar": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "TfwuryVUAE8GLA3ZuTh2VE8gz+15kyBpA+UjMsrgVm9b8biySFegHRNvcdb2H1w/mHYKD6eQbEq/dXGELu1C1A==",
|
"contentHash": "LdDeLE9nmlqbl14RinjII0v8APKZKUqJEClILvdLAoUEdv8UhgsHhDX2BHJKZv2aNfCnb19z9otE8Ffaa11Sow==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.az": {
|
"Humanizer.Core.az": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "CuBTV+VWWkFUdXK0f11KASlx3zAsx701bG/sbhvWWEfEQsXwr8fqNl4S74iNE2TWFSP8e1nPPS4Lu23qpXfwkw==",
|
"contentHash": "+tfsgP5PkTY5mpx7t0rpMVVNXJQMcRkFSsWtnPpntuC/z7I8GS68CBcsHItw5/qbTJxWRY/cLPe/Psxh/SjEkA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.bg": {
|
"Humanizer.Core.bg": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "AFZc1q0s9zFmxlaBYkZLlXWIAy6uxznU424g5RuNllAjuRKIjSZ7VSF/9yRC4PasVZf1hYiqL5M9oQLXz10yqA==",
|
"contentHash": "xBQMDuLOjmj1U0h39tv7B39vmVZXBbRUcy3xkHBKHAyuRoxskDJ3F1LUOOw1/E41+dGQziAe2I0EVukwvQ/h0g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.bn-BD": {
|
"Humanizer.Core.bn": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "nkgVbmYNo9G8brbR0EmVxsN1nF2eqn+GSCMNY3+Nr2VnEo6RlESd8ZYz6+Q93RpelVq2jmsmXWgnp+TLa43eQA==",
|
"contentHash": "U5Li886U/hPdhMxyN9P3eF5dlB15sT/w4F3NOioXsaTQxnJKdxUKglRHiGbakX+aSrsFGok1MoQwVcH7Sac1Lg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Humanizer.Core.ca": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.0.10",
|
||||||
|
"contentHash": "D29T+OWcYG4rlOXKwAGpNNrwLgE44oE1JYqlNuNs3lVYmTDEgItyT+SfjG2TXuTQXkpQLzsxfRjqtA/APygjgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.cs": {
|
"Humanizer.Core.cs": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "ERRbgPC2/Jpj0sSoKFS28dpluDwNteHtmaH2NHq6d9xTbNSNMnINKBR+osDhpFJboJA5EsSEj4ZZwnNDMSnrUw==",
|
"contentHash": "kaG0//ULYkyB8Rk4g5PgC2o8jrmG2joc8lgtyDzCiFCaobr0jgUn393JAIB0JUygbngEIfA0PMK3fLbMXcBNLQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.da": {
|
"Humanizer.Core.da": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "cf6/r5QJ74pQZVNGCr+P0jXJ7WtP8Wqvur053Rwc+Sfvuj+W3Y7A+Bkwnz1kreGhaH5ikz/7tirnKIkdA119cQ==",
|
"contentHash": "/MCePoHsSaRo03I1NSRut5iUzhYRIKL+UncbtpN8AZ3Vl0TOu0Psi9/6QyjD4gLLx45puiXB2S0lUjCdYmlP7w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.de": {
|
"Humanizer.Core.de": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "XdinGY9a+vyZ9HOgqTzH+vs0oFSe8bncw+QlbT9N5dyx9ssTnmo94DhSi9+lAK6TONrv9gFJJdLKLgi2xtvdhw==",
|
"contentHash": "UoJk/DmAOFZivuMulLoB52Pn5iFvSqurF7qQQAyUAz1/IUZjxkoeoiI5mMILEjVeGoQJqrylt6O+f8johCo0Bg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.el": {
|
"Humanizer.Core.el": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "wib7nnI+RS8WapMRUOXxV7tToWwfm6mjytsZ9xIRDlMk+kN1/YgBdKU+A/gPrKsYIBPzHbCtP8PgrdUm1YsnYA==",
|
"contentHash": "5oIHs25LMm1IBrhcxOYYLTFHZQfBaqgKxm0jAkYmsxedpDZkv1k+BHs2D5Kg1672/ZhTs4jsLbqYNXkHm671vw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.es": {
|
"Humanizer.Core.es": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "OBQm6ONiwNgPT1FXYHAdEwgEh/F/C3zu99v/SQVxgUYOP0MxCIjt9heizUOAjkB7iuV7mwC8Ni0TNzDt/CXplQ==",
|
"contentHash": "epxhWWQJ/I610mJGTfQPTocjp7/xCIH6tiREVIlHb/sA1kDAD1zHQcHI+HgWFzn/LzzbVg99y8HVFFHUvjnfXA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.fa": {
|
"Humanizer.Core.fa": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "f1mSmnmTZhEx10qHiYzX7te6cXlF55TwJDrA1bXnIwe5/pEXBYVp9xvQD76zOEOER57HRNaCwum6ZHNkwwquHw==",
|
"contentHash": "9BEFCpbWoUdq2JnLbkohqBfpjjdeMqFhkhCNk3qYmutZwSxuZHrCozppY14SSWI6b6qD1Veb3rVyHGz9Fex5AA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.fi-FI": {
|
"Humanizer.Core.fi": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "M4rtABPYIbJZI41fNbIwY18ly3uwi04pelqG4Ox5PChmDo1KFYeTI+MjqCC3J16CDtV9IYq43JYmz2aS9KkzxQ==",
|
"contentHash": "9Nbbxs1WZH2Li6Q28tZC06Y0mXwmIGnCEO/2nRzHvTqf07153frndezl5ads4uX8mNs+hMnUGUsr03IsFhI3jQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Humanizer.Core.fil": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.0.10",
|
||||||
|
"contentHash": "zpUvc+riMHZ2QGYETwnaDiT4Y4fHv2/Ctpi8Nm5SujNmOuxQyQMEAcalbUqWaLjzfTcysKqlU/NkiNP86kGlCA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.fr": {
|
"Humanizer.Core.fr": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "Zf6DKTXvKUQ4751UuWdpgLvF0sHoWSdU2nYLd0NRkjxkko+GA1VCo3z6jHJKRNFYrOPXYRfl6e1RqXEEnLmhAw==",
|
"contentHash": "k+STDkiJVeO/nA5s0/GeBdwZI17D9KgqyYA4XGxfGxtFACwbFyVKQLWLzlIR2BsrT53EkyKLAzuFJ9FCT2CyfQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
|
||||||
},
|
|
||||||
"Humanizer.Core.fr-BE": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.0.0-beta.96",
|
|
||||||
"contentHash": "6JpbLjoEkYzLTvN9qedAkFW+MCyi8u3zx2DFNur64uLkO5gIKMUUbGLfFT9DJiji9iTukryGejmFGC4gqd5HnA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.he": {
|
"Humanizer.Core.he": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "2EkoQaHlsPYNOvfGwioQfa5PB5tYCuRI2sbVKDd71SujUUuFmvQo7Y1cqrXkB0aRw4IXRMV4kqVw7WC54vQjIw==",
|
"contentHash": "4WIzAg5hocnbnzaTnueYW/BRhCrQoPg/0U6WiUC4BJKGJDuVYbqIa8iVcHqhr7vVKd5ngFbhSsXuCgkYqOMaXQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.hr": {
|
"Humanizer.Core.hr": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "5eKjP6mxl5F7CmD4++u2aRfPkm7Ky1jdC322lOhORXIMY2QUG7nSk6wdP3n4gZX/35GI3ZbCs2SOA67rc8QGiw==",
|
"contentHash": "USin9T/FsHjmxvaolEXgL6FOdw+1Juh7geqcjTvJRCdoo/U4Y/usoAiZRbFtDA//1od4yqSxEbmj+F16nKwbkQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.hu": {
|
"Humanizer.Core.hu": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "Hfyn9yfC4SkQ2IDV2x74oQauosvbXJdg43bzA/W3uPP3gBBKeZHsGnYdZtlWoXcsH9bbBj6PEi2PQwrHI5Euzg==",
|
"contentHash": "Umon7VBTVDs8QFDqNzGm6MBD+43UZJoN44htW/+iJUDF2gl8K8meRup27Dkl2zvewW3ynZic/xwP2hTDaPjIjw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.hy": {
|
"Humanizer.Core.hy": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "noClOyB3R9YfDQ3R8rMFLXTBlb9qwz6oEnDwgOojv+gUQ6uN+39DNEB3NiTAnL9hryC4ArnCbixOjCXghjIr4Q==",
|
"contentHash": "PzdcqQMwyDYO81PYbzBf+gFMj+wzOYF3kz3mlJCcBX5mSmNRpLIwB2s6PC9OiNN/Y/ya1orHV/z0OY9j7NFc5Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.id": {
|
"Humanizer.Core.id": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "6uxBc4LeJxm2UWGjkF4BbXYeaNmy6oM8OVaaDmcMtWWanJNpxQ0q7i0UssuLKeFEOhm2KnDVmTcHqkOS3p26wA==",
|
"contentHash": "rMb9jvglGeeUxb6Z4EkXTg+et1Ri3DWZjlXs2WFle1Qpu39zWWF0l9rh9PQJb0fzeiZ8jOjiUhUI9lRnoAHHCQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.is": {
|
"Humanizer.Core.is": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "DH6j66s1l2OX0rXM22uaFDN6bwh6Ss6LBckaUWUb4t1aEzDIgW09mzZoMG+3zx/LhRibmsur86rf/jN3+2weCQ==",
|
"contentHash": "xnNgWY9azqXyclRYmy3D6kgnbdCcPFBAEN4yNdhu2IN10p7Y2T9YUw5R9jLWo5+NGM2uzbbyTxPGf4+9YYUc1w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.it": {
|
"Humanizer.Core.it": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "WzxjrUGtRWrB623K8M0wW7qmAdfLroind4yEvsPTRnDFa0prFHLFZxOWF9ypBu6yLXR3T2TdT4Z3r7jYsT+bLA==",
|
"contentHash": "iCrKxOX6q/PZgEk3vYyU1fHC0jnq0hiq7nwWXiuV7uF5VypOxwVeao3vkE6MLoNKW7RQN/TbP06n1TOqyhTA7A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ja": {
|
"Humanizer.Core.ja": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "6aHU1JB1gF0azIodYsPFJ5s/5fiDoDiCg5kTI7O+SoOyJrpjKJ8umaMN6r/VwhEUPW1yHxFsEkX/XHO+mXrvAA==",
|
"contentHash": "UicfcXwMibwWN3jLesdk702IRQVndgBUqueFr3iXVEn2POue06pHInSIruxNaPTscaNdGKxXZiIiJrN7pCVO9Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ko-KR": {
|
"Humanizer.Core.ko": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "gcYmdBIZ+oBFRTnDZ3RMx4DIOoLcaTa5XPbmefl0GdDJeG7xsFxZQHeRh5sHXNIiMchSYjkPgCgfJTuiwHumIQ==",
|
"contentHash": "Zr/GwEilhAcmeiDxVKqr79ZK/SEpHfhKzXD15JZ1NpsfYnIfNAN/9i0o535UaadaDDT6EIVpkoGIYAGuYQ0Z4g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ku": {
|
"Humanizer.Core.ku": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "4tMhhNYFOEljlp+y5C/xMVpXga5rGUiYv1gEQJC82tRMhxm/q5elVMQEGtx/iJWZSNiCgj4VZ2Ur4Sqh186gJQ==",
|
"contentHash": "j5Qm+CY+c8onfjO1O+TReCDg/epCYdfv5N5LNB+Ydcmn06gkR4HvbWJnk5UFLVHz8NMPmtCrv7CYwmPJ0+lJqw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.lb": {
|
"Humanizer.Core.lb": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "QAObL6iaSztyoYnA8yZlrGraXKXGcxPufRpDbn7S3+boTXY3tkdK4ytZH61AB6wJvuN2ZVqphOMYgCnttO8DUg==",
|
"contentHash": "5lpkw49T6MQ7Gwa0k2JV4cw6aH5FdT1Ku1nfGeey/iScajHDV9mvdxjkxRZpFAhJ5W/5yCtMlHH992LpXFJkeA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.lt": {
|
"Humanizer.Core.lt": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "ePE7K9tngIOLwbKf6lQjtRhTTpiRUvy/DxnHAXI282RDA9DiUB7OE4Zr6NHJeIdrJA4JGat+HosJDPwDp7vV7A==",
|
"contentHash": "1OO4XZ/74tt5nkxYHnpj+ti1w0HdeMGi+AHR59asxcJ3A/iTDZR4RNRIvoj1MdYRe0HdTUjtnGXfA7tXXOkiYg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.lv": {
|
"Humanizer.Core.lv": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "HL/3ilL1doE8MublQyMFB4ro/C6M7miAfHvtOXUtc7wsbHJPc9HnbIcBhnyxh3JmYgKbThWoZ6ssfmDeV73qbg==",
|
"contentHash": "S4RN1q/saAQxlFlRRUnmi3Oz+mIXRqWtIRZJA3VrvMmBy12cXIdpeY0+d+HCA58WXpI2diOrWxHq9KPUDPunIw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ms-MY": {
|
"Humanizer.Core.ms": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "CuUek2ZvjaR3l4zSsb0lhUujTvtExLkSgP1RlQiaux0RB9Kbh88odwLTDaPhWYcA7D0h0nCkBxSZNwxhP4kWdg==",
|
"contentHash": "m0+BJa9vG0GeuAWhmdmMxdUUa1cfGjyHZQeJoeivqxJrrtdUUFekXH0xbpvI3ErC8qu5Ufpmfnr6QAX0WQfkIw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.mt": {
|
"Humanizer.Core.mt": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "ss4Uxs9mjzaBw5Fhy68ttAP3MeyxHD1Rbk+bLERBMWtwQibER1fcH4JYu/EuRJ7yDe4oObOjRqu3GzNrXGLbXg==",
|
"contentHash": "8GfXTko/iweepZHcUcOANreClfvTeCDfSuiMxLtFbUZQOBegLZ6VnTVun2gVtv5OANdOk64qhqUKIVLq2rG0rw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.nb": {
|
"Humanizer.Core.nb": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "o579CpW97Oj8Xp4IgTzlwieH+QOV/gRbEXUKapaQdiK/bOCOMm9N1z+wNhbDYmY/Uym6K34PG7xpbqio0Nj9QQ==",
|
"contentHash": "UcdV9TWXG2MFVEkqHy8oD2ZbuFhFPLY7ScCJKWBu7OsgInyuHZe72hHP/i52T0FAgiVbrB+Urd3uoWiUT7hP9Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
|
||||||
},
|
|
||||||
"Humanizer.Core.nb-NO": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "3.0.0-beta.96",
|
|
||||||
"contentHash": "epxcGNzQMeyhGNEypcb3bFYePcoARkBHWHfKttOdNKI91uuSMt7lG9/Y7l8cGGVPfvOLu6brU6KZISQ+85TyaA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.nl": {
|
"Humanizer.Core.nl": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "FmFCjnc53MH5tk6sRgOB/pSjFUmCiPBO2OcdV1szqYPJ0dGNkGw3pHUfE3P3gAHYuHL/GdYyhCi74/BiyK5C2g==",
|
"contentHash": "b+Y1CFId1ucl33OvNITID0eEVesOKiw/dmrcDJ4Z/P9KEuMTeE8aKu5/iSjwtPVV+SagaMx9XzZe7kVYyRqhuQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.pl": {
|
"Humanizer.Core.pl": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "JMUCLMrAEzRR9b+4BY4+b8D+ymQIUAp74b7+zWY/ZLOnwc8Yw01sFx51hbJCgohEVXAitOAyHjzvrhUoijG32Q==",
|
"contentHash": "LJCt3+dGUQhopQ/1w/zzBNlUHtjkdrt57zS+llMQeFm+1u0e3R+lWJx4zP+hqZcpRA7armR38KghvRSz+bYBbg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.pt": {
|
"Humanizer.Core.pt": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "Xb9XhdAQKG9qkbR/SPO5oYPiUtDaueTwJPRnUoBBIbdDTzpQlSUVjJ/DFez+/X8d8GsANWjof3g/5YnOWZ16+g==",
|
"contentHash": "+cggEy6W08xD8GU45/EaMfvNQMCd7K1G7hprSPty/9zIoM3OzaajdMA/Oxrb0mHIKpARxCNbQ29ltOtip3MTNA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Humanizer.Core.pt-BR": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.0.10",
|
||||||
|
"contentHash": "kbhwn4G+STHUT9cLDdrP60+ZFla4lTg6def+FTnhpMS4JHf8ll0TL1kz9Nj5DTJFdqnvs8/eYfmVhnEegyBjBw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ro": {
|
"Humanizer.Core.ro": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "SSaZzeQmrbmDh2/CHSUF3jbU2V0ilcVHPa32/i7g0TXeywzP35CIcZ7vfKlJcfLSnempMg1bdm703hfeUdoLTw==",
|
"contentHash": "JwdY+SywNIDrFcWKyiZsgsSJkIleoAXAtCLYiLn+CNCk+135VwhchZHm5B9nNvpLjGVaP5Y0ouZQ8xTx1oeZag==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.ru": {
|
"Humanizer.Core.ru": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "+uTlQsIs4blTP6f0mnQ7lnn8wyOLmZFMWHqqTh3+Ul960FLlo0UnxYScV1sgNLHudv/04IiLsuj3bGxWJKW0FQ==",
|
"contentHash": "1vXr7pkV0zSZF6ENJUadsAagubrBAvTPHPgdgttHlMhUQ1Zz9ZWTJaQdmf2RCen4YywMmfsQQZiGqJl52LVT8Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.sk": {
|
"Humanizer.Core.sk": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "dcSN1GYFUNTQFlLljGZLarLe1e258wtNK+k0Mj2sghN+ffvlK1aRhUvanmqAaURfkK5ume1Q4e0A4/ATMNNFsw==",
|
"contentHash": "LD93nJw1zwXuC+LSw3+02Jk02bdAoENHzb/qrMoGBf70XeV2rBeoxSyJBVLHQ00RZfwNSy6TCTXQyagESVmN+Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.sl": {
|
"Humanizer.Core.sl": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "H05Tl8RQs8RoJ+UWYPga087x4Be57EpRk6iYwo4absTlFyPeISv8whUqayJBvDpNGS94QHennrnfembrCnhJXg==",
|
"contentHash": "utURJ8K/tnbA5KXlKcT8T+35MEVdZrXUvu2tH+T6M9xxJ1MhLGuFWnc687pcsLdpaLnGB7ZVwrAITlueAE+KGA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.sr": {
|
"Humanizer.Core.sr": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "FLH10qbsgL5NJJ/moB1iSnmO1wkVDloJYwq1CwIKp0PePb4bViO0z4nTXKCERWn3zl3WXeCJ5gOUyrvplr6bSQ==",
|
"contentHash": "e2bVzUAjEpxjFdLVmXZL9EsdVojlV/8GMwZzwDS6OByfG+PP5JxemKylK6FoUK5X2IBPNg68LOvn72WgwaruIQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.sr-Latn": {
|
"Humanizer.Core.sr-Latn": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "aTIigE/P5ahO5RO88igcz5V8Fo0MP8+Cu/NFVFXQJv+Yw39dQ1fOscLpVXenOg22sy2Tz2pi/JNfK42SDEiV6Q==",
|
"contentHash": "VXPlm+eg5sraDhZ50WGiMZMvj4I2d+0VAy8cNaLLXNt+EgkUz646dRfg5HrQPPjtBbegj6o7gcg8Xl6z0ke+hA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.sv": {
|
"Humanizer.Core.sv": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "k3URfcuKrOfL64Eg5mE2uV8YotKnhObFsJaIiyGhRPyl2QrrPU6VXKM2DmLa6Cfa1RN3zHYLv8PzBo1g6jVBYA==",
|
"contentHash": "COq4bbUKk7F1jfE2sC/ZmomoQqX0Herwe1iXZyDl5zyw21mUY9wL96S1qbRmRp15UmGzFJdqBPdnd58P7kVMyg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.th-TH": {
|
"Humanizer.Core.th": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "9vaBFcdcXvE4uXr4VLbhPQ/Hv7lKf4Cb1HzThrnWklTk6uzZkmYYRs/yat+apk7of0VAlY8rthsMiNIwwSWW+A==",
|
"contentHash": "8A4lZCLE6bEPYdQKeGfPBHDgAsY30TWWdjoWA1+vAfLA9Ip1l2Ons+Ngw4npkcoI3qdCidU6hWOsFnPfJ8ZufQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.tr": {
|
"Humanizer.Core.tr": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "d5xUVe/1NrmAeBTL3KamGtZJ/3ja7O8OOWV/ICmNeOEHPMg5wZTWnQ3DOOrJAdrnf5D6kNcngKxYwKZYoRS19g==",
|
"contentHash": "aRAxq1VyuIVoFdx8TZyVsRoDk0yqe9NATjhGgWHPsdR8TVHnuiyXeEbiXf846870VteyLJL9tVy3jaj2rC45tw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.uk": {
|
"Humanizer.Core.uk": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "3i9xEIoqoWUtNKaihYqr9EfQw7ilWiDIaDwrXDrADldO1HHFAcc+F2vNEB1sZ81/F/1FBCthyODTFDBD1Ma4Kg==",
|
"contentHash": "NPnGZ1jdoMDYGrsHt5aLlj7Ambp7i5ER/5BbtjBSegn3+PliMbd4Eftr1Ind4qvCU6rTP2a9lUqBoRd+hSVA+g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.uz-Cyrl-UZ": {
|
"Humanizer.Core.uz-Cyrl-UZ": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "LZK6cDgU0fQ0zURLYwk4qlodc0ZGcXkxV6zfHJbHXBsow12cJ2p7NhL495ihWLnDrHfD20+J1y9rTvC9vIWnFA==",
|
"contentHash": "8NSyuiTlCfse1ki9QeO35IpZLcxoMlSMGvEY892aw9CNGaDMROQjFzf1iKJkWTyB9atrHHPuWlasBAFk2QkT9w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.uz-Latn-UZ": {
|
"Humanizer.Core.uz-Latn-UZ": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "AmoGiYjAjoi7htH6Vimd266rGRfyU0Jsa9BjALP7EAsWILjAzba3aMmChK33A6apFzC9fv2XT0amZsgIqBvR/Q==",
|
"contentHash": "FaSETkwfUiDU/RHlD54xbBKEZXnFIUnTNTnA5ZYXgDOiMmX6cY7vyRDmI1YpQtO+QR1irSettpLFpHeysslnpA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.vi": {
|
"Humanizer.Core.vi": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "PZnFdIJF54ODyMazZDSe30NuRuVx4/c/3KBXzwFX3tR+yz+gJkYlYpTI8ozRLF2/mlcFr9iFAU4Pau6jJFevFw==",
|
"contentHash": "Uz5gph//0eNP0Ta1tqdKDyghiUnZdW0aVW4MK/poD5hLEH1qLoDTWuTb8gxj3F8GvSFjgVmF9HZGQx5cO06N8Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.zh-CN": {
|
"Humanizer.Core.zh-CN": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "GX8JvHC9/ghN8ZU/MadCnRrLB6kJXQEuJz3i0hS03VVrTZlHMRQfXEC3NhSktSJ9FCyYq+WZaw7jhmSaQ/S2pA==",
|
"contentHash": "7veG4JdZ4OeYNlgci/6I/Xf9G/iCljBcwsbLVyGaqdia+2UMAp1iGu+MCs3zKOmEvcb4ipHGFkwT5d/2WfEevA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.zh-Hans": {
|
"Humanizer.Core.zh-Hans": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "S0nzvET+5M188jPN2cUiMguIszKZQ6tVyvAem3xBjvF0LjodAlAmZNlMjw5zaPMACuWMkOgvpKjuA8dhFa2Gqg==",
|
"contentHash": "z8E2R0QL43NB6m8ntQaL1cONMjudoIJXvMQUFXG4OaKp5iyYqWs6FMC2B7x2rLxOynpsxoq5AQ/8BO0pMdfZrw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core.zh-Hant": {
|
"Humanizer.Core.zh-Hant": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.0.0-beta.96",
|
"resolved": "3.0.10",
|
||||||
"contentHash": "nzWjsEb7JLHtBOuWuKBI1b0rm31C4Q8DSJHZy6UPhjO1HVra7GcMVeNyxXsljaVtdjR5kjLaPuhrr0yDNEUtYA==",
|
"contentHash": "KI5Nv82v9diPBxcBAwIAvZQHjSrDy7He0atnzdZghS+QchHwlkPKhTyEzVSbOZdD42RxCX1ArMiL864A2iw34Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
"Humanizer.Core": "[3.0.10]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.AspNetCore.Authorization": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "WgLlLBlMczb2+QLNG6sM95OUZ0EBztz60k/N75tjIgpyu0SdpIfYytAmX/7JJAjRTZF0c/CrWaQV+SH9FuGsrA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Metadata": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Options": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Components": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "6pwfbQKNtvPkbF4tCGiAKGyt6BVpu58xAXz7u2YXcUKTNmNxrymbG1mEyMc0EPzVdnquDDqTyfXM3mC1EJycxQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Authorization": "9.0.1",
|
|
||||||
"Microsoft.AspNetCore.Components.Analyzers": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Components.Analyzers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "I8Rs4LXT5UQxM5Nin2+Oj8aSY2heszSZ3EyTLgt3mxmfiRPrVO7D8NNSsf1voI2Gb0qFJceof/J5c9E+nfNuHw=="
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Components.Forms": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "KyULVU32bLz74LWDwPEwNUEllTehzWJuM7YAsz80rMKEzvR0K8cRjRzO0fnN/nfydMeLRRlbI0xj8wnEAymLVw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Components": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Components.Web": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "LI0vjYEd9MaDZPDQxPCn4gGYDkEC5U9rp1nWZo7rPozJxgTG2zU3WERujxTi2LeAC2ZzdXlOVCrUyPQ55LZV2A==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.AspNetCore.Components": "9.0.1",
|
|
||||||
"Microsoft.AspNetCore.Components.Forms": "9.0.1",
|
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.1",
|
|
||||||
"Microsoft.JSInterop": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.AspNetCore.Metadata": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "EZnHifamF7IFEIyjAKMtJM3I/94OIe72i3P09v5oL0twmsmfQwal6Ni3m8lbB5mge3jWFhMozeW+rUdRSqnXRQ=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "YIMO9T3JL8MeEXgVozKt2v79hquo/EFtnY0vgxmLnUvk1Rei/halI7kOWZL2RBeV9FMGzgM9LZA8CVaNwFMaNA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Binder": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "v5R638eNMxksfXb7MFnkPwLPp+Ym4W/SIGNuoe8qFVVyvygQD5DdLusybmYSJEr9zc1UzWzim/ATKeIOVvOFDg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.FileExtensions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "4EK93Jcd2lQG4GY6PAw8jGss0ZzFP0vPc1J85mES5fKNuDTqgFXHba9onBw2s18fs3I4vdo2AWyfD1mPAxWSQQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.0",
|
|
||||||
"Microsoft.Extensions.FileProviders.Physical": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Configuration.Json": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "WiTK0LrnsqmedrbzwL7f4ZUo+/wByqy2eKab39I380i2rd8ImfCRMrtkqJVGDmfqlkP/YzhckVOwPc5MPrSNpg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Configuration": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Configuration.FileExtensions": "9.0.0",
|
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "qZI42ASAe3hr2zMSA6UjM92pO1LeDq5DcwkgSowXXPY8I56M76pEKrnmsKKbxagAf39AJxkH2DY4sb72ixyOrg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.FileProviders.Physical": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "3+ZUSpOSmie+o8NnLIRqCxSh65XL/ExU7JYnFOg58awDRlY3lVpZ9A369jkoZL1rpsq7LDhEfkn2ghhGaY1y5Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.0",
|
|
||||||
"Microsoft.Extensions.FileSystemGlobbing": "9.0.0",
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.FileSystemGlobbing": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.0",
|
|
||||||
"contentHash": "jGFKZiXs2HNseK3NK/rfwHNNovER71jSj4BD1a/649ml9+h6oEtYd0GSALZDNW8jZ2Rh+oAeadOa6sagYW1F2A=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Localization": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "UgvX4Yb2T3tEsKT30ktZr0H7kTRPapCgEH0bdTwxiEGSdA39/hAQMvvb+vgHpqmevDU5+puyI9ujRkmmbF946w==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Localization.Abstractions": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Options": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Localization.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "CABog43lyaZQMjmlktuImCy6zmAzRBaXqN81uPaMQjlp//ISDVYItZPh6KWpWRF4MY/B67X5oDc3JTUpfdocZw=="
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Options": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "nggoNKnWcsBIAaOWHA+53XZWrslC7aGeok+aR+epDPRy7HI7GwMnGZE8yEsL2Onw7kMOHVHwKcsDls1INkNUJQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
|
||||||
"Microsoft.Extensions.Primitives": "9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Microsoft.Extensions.Primitives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
|
|
||||||
},
|
|
||||||
"Microsoft.JSInterop": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "9.0.1",
|
|
||||||
"contentHash": "/xBwIfb0YoC2Muv6EsHjxpqZw2aKv94+i0g0FWZvqvGv3DeAy+8wipAuECVvKYEs2EIclRD41bjajHLoD6mTtw=="
|
|
||||||
},
|
|
||||||
"Newtonsoft.Json": {
|
"Newtonsoft.Json": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "13.0.1",
|
"resolved": "13.0.1",
|
||||||
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
|
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
|
||||||
},
|
},
|
||||||
"System.Buffers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.1",
|
|
||||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
|
||||||
},
|
|
||||||
"System.Threading.Tasks.Extensions": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.5.4",
|
|
||||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
|
|
||||||
},
|
|
||||||
"caddymanager.contracts": {
|
"caddymanager.contracts": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
@@ -714,14 +541,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"CaddyManager.Contracts": "[1.0.0, )",
|
"CaddyManager.Contracts": "[1.0.0, )",
|
||||||
"Docker.DotNet": "[3.125.15, )",
|
"Docker.DotNet": "[3.125.15, )",
|
||||||
"Humanizer": "[3.0.0-beta.96, )",
|
"Humanizer": "[3.0.10, )",
|
||||||
"Microsoft.Extensions.Configuration": "[9.0.0, )",
|
|
||||||
"Microsoft.Extensions.Configuration.Binder": "[9.0.0, )",
|
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "[9.0.0, )",
|
|
||||||
"Microsoft.Extensions.Configuration.Json": "[9.0.0, )",
|
|
||||||
"NetCore.AutoRegisterDi": "[2.2.1, )"
|
"NetCore.AutoRegisterDi": "[2.2.1, )"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"net10.0/linux-musl-x64": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
49
README.md
49
README.md
@@ -161,8 +161,14 @@ services:
|
|||||||
# Path to the Caddyfile as seen from inside the caddy container. Must match the container side of the
|
# Path to the Caddyfile as seen from inside the caddy container. Must match the container side of the
|
||||||
# caddy container's config volume. Defaults to /etc/caddy/Caddyfile, so this line is optional above.
|
# caddy container's config volume. Defaults to /etc/caddy/Caddyfile, so this line is optional above.
|
||||||
DockerService__CaddyConfigPathInContainer: "/etc/caddy/Caddyfile"
|
DockerService__CaddyConfigPathInContainer: "/etc/caddy/Caddyfile"
|
||||||
|
# Shared key for the HTTP API (see "HTTP API" below). Leave it out to keep the API closed.
|
||||||
|
Api__Key: "change-me"
|
||||||
# To have the access to the caddy config file
|
# To have the access to the caddy config file
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
|
# The .NET GC sizes its heap against the cgroup limit, so this both caps the worst
|
||||||
|
# case and makes the runtime self-tune downward. Raise it if you run many configs.
|
||||||
|
mem_limit: 256m
|
||||||
|
memswap_limit: 256m
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -186,6 +192,49 @@ Currently, the Caddy Manager is able to:
|
|||||||
configuration first, so a broken file is reported back instead of taking the proxy down)
|
configuration first, so a broken file is reported back instead of taking the proxy down)
|
||||||
- Restart caddy container on demand
|
- Restart caddy container on demand
|
||||||
- Parse simple information from the caddy configurations
|
- Parse simple information from the caddy configurations
|
||||||
|
- Do all of the above over HTTP, for scripts and other services (see below)
|
||||||
|
|
||||||
|
### HTTP API
|
||||||
|
|
||||||
|
The same operations are exposed as a JSON API, documented with OpenAPI:
|
||||||
|
|
||||||
|
- Interactive documentation: `/scalar`
|
||||||
|
- OpenAPI document: `/openapi/v1.json`
|
||||||
|
|
||||||
|
Every request needs the shared key in the `X-Api-Key` header. The key comes from `Api:Key`
|
||||||
|
(environment variable `Api__Key`). **While no key is configured the API is disabled and every
|
||||||
|
endpoint answers `503`** — nothing is exposed by accident.
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET` | `/api/configurations` | List the reverse proxy configurations |
|
||||||
|
| `GET` | `/api/configurations/{name}` | Get one configuration with its raw content |
|
||||||
|
| `POST` | `/api/configurations` | Create a configuration (`{ "fileName": "...", "content": "..." }`) |
|
||||||
|
| `PUT` | `/api/configurations/{name}` | Replace a configuration's content (`{ "content": "..." }`) |
|
||||||
|
| `POST` | `/api/configurations/{name}/rename` | Rename a configuration (`{ "newFileName": "..." }`) |
|
||||||
|
| `DELETE` | `/api/configurations/{name}` | Delete a configuration |
|
||||||
|
| `GET` | `/api/caddyfile` | Get the global Caddyfile |
|
||||||
|
| `PUT` | `/api/caddyfile` | Replace the global Caddyfile (`{ "content": "..." }`) |
|
||||||
|
| `POST` | `/api/caddy/reload` | Graceful `caddy reload` |
|
||||||
|
| `POST` | `/api/caddy/restart` | Restart the Caddy container |
|
||||||
|
|
||||||
|
`{name}` is the file name without the `.caddy` extension, as shown in the UI.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -H "X-Api-Key: change-me" http://localhost:8080/api/configurations
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8080/api/configurations \
|
||||||
|
-H "X-Api-Key: change-me" -H "Content-Type: application/json" \
|
||||||
|
-d '{"fileName":"example","content":"example.com {\n\treverse_proxy 10.0.0.2:8080\n}"}'
|
||||||
|
|
||||||
|
curl -X POST -H "X-Api-Key: change-me" http://localhost:8080/api/caddy/reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Renaming moves the file only; if the global Caddyfile imports the old name, update that import
|
||||||
|
yourself (the UI warns about this too).
|
||||||
|
|
||||||
|
> Note: the app redirects HTTP to HTTPS, so a direct `curl http://...` against the container port
|
||||||
|
> gets a `307`. Add `-L`, call it over HTTPS, or go through your reverse proxy.
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,13 @@
|
|||||||
ASPNETCORE_ENVIRONMENT: "Production"
|
ASPNETCORE_ENVIRONMENT: "Production"
|
||||||
CaddyService__ConfigDir: "/config"
|
CaddyService__ConfigDir: "/config"
|
||||||
DockerService__CaddyContainerName: "caddy"
|
DockerService__CaddyContainerName: "caddy"
|
||||||
|
# Shared key for the HTTP API. While unset the /api endpoints return 503 and stay closed.
|
||||||
|
# Api__Key: "change-me"
|
||||||
user: "1000:1000"
|
user: "1000:1000"
|
||||||
|
# The .NET GC sizes its heap against the cgroup limit, so this both caps the
|
||||||
|
# worst case and makes the runtime self-tune downward.
|
||||||
|
mem_limit: 256m
|
||||||
|
memswap_limit: 256m
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ exec docker run --rm \
|
|||||||
--volume "$repo_root":/src \
|
--volume "$repo_root":/src \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
--volume caddymanager-nuget:/root/.nuget/packages \
|
--volume caddymanager-nuget:/root/.nuget/packages \
|
||||||
mcr.microsoft.com/dotnet/sdk:9.0 \
|
mcr.microsoft.com/dotnet/sdk:10.0 \
|
||||||
dotnet "$@"
|
dotnet "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user