Compare commits
7 Commits
6f6a37d5a6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f0019592e | |||
| 4c271241d7 | |||
| 9f7cb79bad | |||
| a94abca127 | |||
| 96f0112449 | |||
| 7380680230 | |||
| b5cbad3664 |
@@ -22,15 +22,15 @@ jobs:
|
||||
config-inline: |
|
||||
[registry."${{ vars.DOCKER_GITEA_DOMAIN }}"]
|
||||
http = true
|
||||
insecure = true
|
||||
insecure = true
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-dotnet@v3
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9' # SDK Version to use.
|
||||
dotnet-version: '10' # SDK Version to use.
|
||||
dotnet-quality: 'ga'
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
- name: Restore dependencies
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore --locked-mode
|
||||
- name: Application metadata
|
||||
id: metadata
|
||||
@@ -50,10 +50,3 @@ jobs:
|
||||
--configuration Release --os linux --arch x64 \
|
||||
/t:PublishContainer -p ContainerRegistry=${{ vars.DOCKER_GITEA_DOMAIN }} \
|
||||
-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 }}"}'
|
||||
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -25,12 +25,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '9' # SDK Version to use.
|
||||
dotnet-version: '10' # SDK Version to use.
|
||||
dotnet-quality: 'ga'
|
||||
cache: true
|
||||
cache-dependency-path: '**/packages.lock.json'
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore --locked-mode
|
||||
- name: Restore dependencies
|
||||
# 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
|
||||
id: metadata
|
||||
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
|
||||
- name: Publish container
|
||||
run: |
|
||||
dotnet publish \
|
||||
--configuration Release --os linux --arch x64 \
|
||||
dotnet publish CaddyManager/CaddyManager.csproj \
|
||||
--configuration Release --os linux-musl --arch x64 \
|
||||
/t:PublishContainer -p ContainerRegistry=ghcr.io \
|
||||
-p ContainerRepository=${{ github.repository }} -p:ContainerImageTags='"${{ steps.metadata.outputs.APP_VERSION }};latest"'
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</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,
|
||||
request.FileName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{request.FileName}.caddy");
|
||||
// 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)
|
||||
{
|
||||
if (configurationName != CaddyGlobalConfigName && IsInvalidFileName(configurationName))
|
||||
{
|
||||
failed.Add(configurationName);
|
||||
continue;
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(Configurations.ConfigDir,
|
||||
configurationName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{configurationName}.caddy");
|
||||
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.10" />
|
||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||
<PackageReference Include="Humanizer" Version="3.0.0-beta.96" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
||||
<PackageReference Include="Humanizer" Version="3.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -19,6 +19,18 @@ public class ConfigurationsService(IConfiguration configuration) : IConfiguratio
|
||||
else if (section.EndsWith("Configuration"))
|
||||
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">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
@@ -21,17 +31,17 @@
|
||||
</None>
|
||||
</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="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||
<PackageReference Include="AwesomeAssertions" Version="9.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -41,7 +51,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<Using Include="FluentAssertions" />
|
||||
<Using Include="AwesomeAssertions" />
|
||||
<Using Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ Coverage reports exclude:
|
||||
## Test Frameworks and Libraries
|
||||
|
||||
- **xUnit**: Primary testing framework
|
||||
- **FluentAssertions**: For readable assertions
|
||||
- **AwesomeAssertions**: For readable assertions
|
||||
- **Moq**: For mocking dependencies
|
||||
- **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
|
||||
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
|
||||
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
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
@@ -436,6 +436,37 @@ public class CaddyServiceTests : IDisposable
|
||||
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
|
||||
|
||||
#region SaveCaddyGlobalConfiguration Tests
|
||||
@@ -755,6 +786,37 @@ public class CaddyServiceTests : IDisposable
|
||||
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
|
||||
|
||||
#region GetCaddyConfigurationInfo Tests
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<Exclude>[*]Microsoft.*</Exclude>
|
||||
<Exclude>[*]System.*</Exclude>
|
||||
<Exclude>[*]Moq.*</Exclude>
|
||||
<Exclude>[*]FluentAssertions.*</Exclude>
|
||||
<Exclude>[*]AwesomeAssertions.*</Exclude>
|
||||
<Exclude>[*]xunit.*</Exclude>
|
||||
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
|
||||
<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">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<LangVersion>13</LangVersion>
|
||||
<LangVersion>14</LangVersion>
|
||||
<ContainerRepository>caddy-manager</ContainerRepository>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<!-- To avoid error on lock file not found -->
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
@@ -19,11 +40,15 @@
|
||||
</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="Humanizer" Version="3.0.0-beta.96" />
|
||||
<PackageReference Include="MudBlazor" Version="8.0.0" />
|
||||
<PackageReference Include="Humanizer" Version="3.0.10" />
|
||||
<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="Scalar.AspNetCore" Version="2.16.16" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -21,8 +21,8 @@ public partial class MainLayout
|
||||
if (firstRender)
|
||||
{
|
||||
// Get the system preference for dark mode
|
||||
_isDarkMode = await _mudThemeProvider.GetSystemPreference();
|
||||
await _mudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
|
||||
_isDarkMode = await _mudThemeProvider.GetSystemDarkModeAsync();
|
||||
await _mudThemeProvider.WatchSystemDarkModeAsync(OnSystemPreferenceChanged);
|
||||
_isInitialing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
@bind-SelectedValues="_selectedCaddyConfigurations">
|
||||
@foreach (var (index, caddyConfig) in _availableCaddyConfigurations.Index())
|
||||
{
|
||||
<CaddyReverseProxyItem ConfigurationInfo="@caddyConfig" OnCaddyRestartRequired="@ReloadCaddy"
|
||||
OnCaddyfileDuplicateRequested="@HandleDuplicateRequest" />
|
||||
<CaddyReverseProxyItem ConfigurationInfo="@caddyConfig"
|
||||
OnEditRequested="@(fileName => ShowCaddyfileEditorDialog(fileName))" />
|
||||
|
||||
@if (index < _availableCaddyConfigurations.Count - 1)
|
||||
{
|
||||
|
||||
@@ -63,7 +63,8 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
||||
/// <returns></returns>
|
||||
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
|
||||
{
|
||||
FullWidth = true,
|
||||
@@ -71,7 +72,8 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
||||
}, parameters: new DialogParameters<CaddyfileEditorComponent>
|
||||
{
|
||||
{ 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;
|
||||
@@ -81,6 +83,7 @@ public partial class CaddyReverseProxiesPage : ComponentBase
|
||||
await ReloadCaddy();
|
||||
}
|
||||
|
||||
// Always rebuild the list, the configuration may have been renamed or had its hostnames and ports changed
|
||||
Refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using CaddyManager.Contracts.Caddy;
|
||||
using CaddyManager.Contracts.Models.Caddy;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace CaddyManager.Components.Pages.Caddy.CaddyReverseProxies;
|
||||
|
||||
@@ -12,52 +10,18 @@ namespace CaddyManager.Components.Pages.Caddy.CaddyReverseProxies;
|
||||
public partial class CaddyReverseProxyItem : ComponentBase
|
||||
{
|
||||
/// <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>
|
||||
[Parameter]
|
||||
public EventCallback OnCaddyRestartRequired { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnCaddyfileDuplicateRequested { get; set; }
|
||||
public EventCallback<string> OnEditRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public CaddyConfigurationInfo ConfigurationInfo { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Dialog service for showing the Caddy file editor dialog
|
||||
/// </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
|
||||
/// Request the Caddy file editor dialog for this configuration
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task Edit()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
private Task Edit() => OnEditRequested.InvokeAsync(ConfigurationInfo.FileName);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
<MudButton OnClick="Duplicate">Duplicate</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>
|
||||
</MudDialog>
|
||||
@@ -144,13 +144,13 @@ public partial class CaddyfileEditor : ComponentBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the Caddy configuration file and restarts the Caddy container
|
||||
/// Saves the Caddy configuration file and reloads the Caddy configuration
|
||||
/// </summary>
|
||||
private async Task SaveAndRestart()
|
||||
private async Task SaveAndReload()
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public class ApplicationInfo
|
||||
/// <summary>
|
||||
/// The version of the application, to be defined and tagged
|
||||
/// </summary>
|
||||
public static readonly string Version = "1.1.0";
|
||||
public static readonly string Version = "1.2.2";
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
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
|
||||
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"
|
||||
COPY . .
|
||||
WORKDIR "/src/CaddyManager"
|
||||
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
|
||||
RUN dotnet publish "CaddyManager/CaddyManager.csproj" -c $BUILD_CONFIGURATION -o /app/publish --no-restore /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "CaddyManager.dll"]
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using CaddyManager.Api;
|
||||
using CaddyManager.Components;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.OpenApi;
|
||||
using MudBlazor.Services;
|
||||
using NetCore.AutoRegisterDi;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services
|
||||
.AddMudServices()
|
||||
.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
@@ -15,7 +19,56 @@ builder.Services.RegisterAssemblyPublicNonGenericClasses(System.Reflection.Assem
|
||||
.Where(t => t.Name.EndsWith("Service"))
|
||||
.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 =>
|
||||
{
|
||||
@@ -26,6 +79,9 @@ builder.Services.AddMudServices(config =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Has to run before anything that reads the scheme, host or client address
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -43,4 +99,8 @@ app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.MapCaddyApi();
|
||||
app.MapOpenApi();
|
||||
app.MapScalarApiReference();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Api": {
|
||||
"Key": ""
|
||||
},
|
||||
"CaddyService": {
|
||||
"ConfigDir": "/root/compose/caddy/config"
|
||||
},
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net9.0": {
|
||||
"net10.0": {
|
||||
"BlazorMonaco": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.3.0, )",
|
||||
"resolved": "3.3.0",
|
||||
"contentHash": "ywtUCZMfmaNBadhQbZEGPerdptHUcyVbPB2Ug1HTNzarJlMCJFSBRvQZefS383h/vDhFgHUrR7/pE6scIc6lJg==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Components": "9.0.0",
|
||||
"Microsoft.AspNetCore.Components.Web": "9.0.0"
|
||||
}
|
||||
"requested": "[3.5.0, )",
|
||||
"resolved": "3.5.0",
|
||||
"contentHash": "MfEzmLlhGEUCi4O65jnDipDUf0UZz3sO9tWupp09hMSvHWcYIP7NRG6r3PIfBUz78nfEkU+aW7Jyg71gd8QcPQ=="
|
||||
},
|
||||
"Docker.DotNet": {
|
||||
"type": "Direct",
|
||||
@@ -18,694 +14,525 @@
|
||||
"resolved": "3.125.15",
|
||||
"contentHash": "XN8FKxVv8Mjmwu104/Hl9lM61pLY675s70gzwSj8KR5pwblo8HfWLcCuinh9kYsqujBkMH4HVRCEcRuU6al4BQ==",
|
||||
"dependencies": {
|
||||
"Newtonsoft.Json": "13.0.1",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
"Newtonsoft.Json": "13.0.1"
|
||||
}
|
||||
},
|
||||
"Humanizer": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.0-beta.96, )",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "T1X21b+0l3jYDH1DztroJIeXdCwlNGmNYDidru9TzcLahwceQ48UGMjOQeWSa1v8zncPvhJzLzVAYWmOZcOySA==",
|
||||
"requested": "[3.0.10, )",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "13+6RiWdpVCzSn2SSEd1hAIr/UHScNdLCUhRgjvI60UcD5u3vXu3u1/Z+74OcHWum1itjiBTjYXgJKQna1jTgw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core.af": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ar": "3.0.0-beta.96",
|
||||
"Humanizer.Core.az": "3.0.0-beta.96",
|
||||
"Humanizer.Core.bg": "3.0.0-beta.96",
|
||||
"Humanizer.Core.bn-BD": "3.0.0-beta.96",
|
||||
"Humanizer.Core.cs": "3.0.0-beta.96",
|
||||
"Humanizer.Core.da": "3.0.0-beta.96",
|
||||
"Humanizer.Core.de": "3.0.0-beta.96",
|
||||
"Humanizer.Core.el": "3.0.0-beta.96",
|
||||
"Humanizer.Core.es": "3.0.0-beta.96",
|
||||
"Humanizer.Core.fa": "3.0.0-beta.96",
|
||||
"Humanizer.Core.fi-FI": "3.0.0-beta.96",
|
||||
"Humanizer.Core.fr": "3.0.0-beta.96",
|
||||
"Humanizer.Core.fr-BE": "3.0.0-beta.96",
|
||||
"Humanizer.Core.he": "3.0.0-beta.96",
|
||||
"Humanizer.Core.hr": "3.0.0-beta.96",
|
||||
"Humanizer.Core.hu": "3.0.0-beta.96",
|
||||
"Humanizer.Core.hy": "3.0.0-beta.96",
|
||||
"Humanizer.Core.id": "3.0.0-beta.96",
|
||||
"Humanizer.Core.is": "3.0.0-beta.96",
|
||||
"Humanizer.Core.it": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ja": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ko-KR": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ku": "3.0.0-beta.96",
|
||||
"Humanizer.Core.lb": "3.0.0-beta.96",
|
||||
"Humanizer.Core.lt": "3.0.0-beta.96",
|
||||
"Humanizer.Core.lv": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ms-MY": "3.0.0-beta.96",
|
||||
"Humanizer.Core.mt": "3.0.0-beta.96",
|
||||
"Humanizer.Core.nb": "3.0.0-beta.96",
|
||||
"Humanizer.Core.nb-NO": "3.0.0-beta.96",
|
||||
"Humanizer.Core.nl": "3.0.0-beta.96",
|
||||
"Humanizer.Core.pl": "3.0.0-beta.96",
|
||||
"Humanizer.Core.pt": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ro": "3.0.0-beta.96",
|
||||
"Humanizer.Core.ru": "3.0.0-beta.96",
|
||||
"Humanizer.Core.sk": "3.0.0-beta.96",
|
||||
"Humanizer.Core.sl": "3.0.0-beta.96",
|
||||
"Humanizer.Core.sr": "3.0.0-beta.96",
|
||||
"Humanizer.Core.sr-Latn": "3.0.0-beta.96",
|
||||
"Humanizer.Core.sv": "3.0.0-beta.96",
|
||||
"Humanizer.Core.th-TH": "3.0.0-beta.96",
|
||||
"Humanizer.Core.tr": "3.0.0-beta.96",
|
||||
"Humanizer.Core.uk": "3.0.0-beta.96",
|
||||
"Humanizer.Core.uz-Cyrl-UZ": "3.0.0-beta.96",
|
||||
"Humanizer.Core.uz-Latn-UZ": "3.0.0-beta.96",
|
||||
"Humanizer.Core.vi": "3.0.0-beta.96",
|
||||
"Humanizer.Core.zh-CN": "3.0.0-beta.96",
|
||||
"Humanizer.Core.zh-Hans": "3.0.0-beta.96",
|
||||
"Humanizer.Core.zh-Hant": "3.0.0-beta.96"
|
||||
"Humanizer.Core.af": "3.0.10",
|
||||
"Humanizer.Core.ar": "3.0.10",
|
||||
"Humanizer.Core.az": "3.0.10",
|
||||
"Humanizer.Core.bg": "3.0.10",
|
||||
"Humanizer.Core.bn": "3.0.10",
|
||||
"Humanizer.Core.ca": "3.0.10",
|
||||
"Humanizer.Core.cs": "3.0.10",
|
||||
"Humanizer.Core.da": "3.0.10",
|
||||
"Humanizer.Core.de": "3.0.10",
|
||||
"Humanizer.Core.el": "3.0.10",
|
||||
"Humanizer.Core.es": "3.0.10",
|
||||
"Humanizer.Core.fa": "3.0.10",
|
||||
"Humanizer.Core.fi": "3.0.10",
|
||||
"Humanizer.Core.fil": "3.0.10",
|
||||
"Humanizer.Core.fr": "3.0.10",
|
||||
"Humanizer.Core.he": "3.0.10",
|
||||
"Humanizer.Core.hr": "3.0.10",
|
||||
"Humanizer.Core.hu": "3.0.10",
|
||||
"Humanizer.Core.hy": "3.0.10",
|
||||
"Humanizer.Core.id": "3.0.10",
|
||||
"Humanizer.Core.is": "3.0.10",
|
||||
"Humanizer.Core.it": "3.0.10",
|
||||
"Humanizer.Core.ja": "3.0.10",
|
||||
"Humanizer.Core.ko": "3.0.10",
|
||||
"Humanizer.Core.ku": "3.0.10",
|
||||
"Humanizer.Core.lb": "3.0.10",
|
||||
"Humanizer.Core.lt": "3.0.10",
|
||||
"Humanizer.Core.lv": "3.0.10",
|
||||
"Humanizer.Core.ms": "3.0.10",
|
||||
"Humanizer.Core.mt": "3.0.10",
|
||||
"Humanizer.Core.nb": "3.0.10",
|
||||
"Humanizer.Core.nl": "3.0.10",
|
||||
"Humanizer.Core.pl": "3.0.10",
|
||||
"Humanizer.Core.pt": "3.0.10",
|
||||
"Humanizer.Core.pt-BR": "3.0.10",
|
||||
"Humanizer.Core.ro": "3.0.10",
|
||||
"Humanizer.Core.ru": "3.0.10",
|
||||
"Humanizer.Core.sk": "3.0.10",
|
||||
"Humanizer.Core.sl": "3.0.10",
|
||||
"Humanizer.Core.sr": "3.0.10",
|
||||
"Humanizer.Core.sr-Latn": "3.0.10",
|
||||
"Humanizer.Core.sv": "3.0.10",
|
||||
"Humanizer.Core.th": "3.0.10",
|
||||
"Humanizer.Core.tr": "3.0.10",
|
||||
"Humanizer.Core.uk": "3.0.10",
|
||||
"Humanizer.Core.uz-Cyrl-UZ": "3.0.10",
|
||||
"Humanizer.Core.uz-Latn-UZ": "3.0.10",
|
||||
"Humanizer.Core.vi": "3.0.10",
|
||||
"Humanizer.Core.zh-CN": "3.0.10",
|
||||
"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": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.0, )",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "itY10dugofdvLYnJ1VZSzUIiKVqNvnTz4eEMYlHUdrOfsk9xShj7k1EW5H6ssMmkbQzShlLBHlUt+VPFGoEJGw==",
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Components": "9.0.1",
|
||||
"Microsoft.AspNetCore.Components.Web": "9.0.1",
|
||||
"Microsoft.Extensions.Localization": "9.0.1"
|
||||
}
|
||||
"requested": "[9.7.0, )",
|
||||
"resolved": "9.7.0",
|
||||
"contentHash": "uWOgNn9B556J/TL8KFoSqpA3YD2heqOpK5etcofwlj1RiEX1IDy0NmwYE9urlTUQ8zProEK5GqwTBnoObFPN3g=="
|
||||
},
|
||||
"NetCore.AutoRegisterDi": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.2.1, )",
|
||||
"resolved": "2.2.1",
|
||||
"contentHash": "qRda/VP+Lxak/GCGfT3PqXE6VA+bCbf2wlExcUWwnkwY1d6cfWv4Fp5RRN6dChlFhI8tbmzlNutYwxlA8kBb1A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "2.1.1"
|
||||
}
|
||||
"contentHash": "qRda/VP+Lxak/GCGfT3PqXE6VA+bCbf2wlExcUWwnkwY1d6cfWv4Fp5RRN6dChlFhI8tbmzlNutYwxlA8kBb1A=="
|
||||
},
|
||||
"Scalar.AspNetCore": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.16.16, )",
|
||||
"resolved": "2.16.16",
|
||||
"contentHash": "Ax0e0bIh+Upf92k1+pTBUom3e/kbpu20qsrDYmmS1NM721Eq2xF8c789x6IbiNrvW8WwySRzPv/icYYhb6idSg=="
|
||||
},
|
||||
"Humanizer.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "anMn7QrFRWthyt08MSDFdUKoOwMqNqDpI9AJ3YIgJZJq1qmJd9sjHiKgJM/Ov0WyLEE8pARzyxOkIbXX1pBd0w=="
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "yZIhtw8sYuvsONzQbZxWpR60tMWYHXoo0DL6nyOqSFiU5POjBTSEyWFpTQtJEZuy+oqiYTXKXY/Mjx7KnqIQFw=="
|
||||
},
|
||||
"Humanizer.Core.af": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "8/9iF3PY2Dm5M87AyU02LoUZV634znBVQsuMQu+yjxtAgGYAksL0skikU1IblDEp2Hh7I2ejOa48Jb/MjIvSEw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "jIpC73aOXfhqGEnpFKgO2AJ2Esb2t40c/cKqR+RgK1yg1bcBZBuhtjs+PrAIHf5Lp7Znd4teLjCGDS5ut8O1cw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.ar": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "TfwuryVUAE8GLA3ZuTh2VE8gz+15kyBpA+UjMsrgVm9b8biySFegHRNvcdb2H1w/mHYKD6eQbEq/dXGELu1C1A==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "LdDeLE9nmlqbl14RinjII0v8APKZKUqJEClILvdLAoUEdv8UhgsHhDX2BHJKZv2aNfCnb19z9otE8Ffaa11Sow==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.az": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "CuBTV+VWWkFUdXK0f11KASlx3zAsx701bG/sbhvWWEfEQsXwr8fqNl4S74iNE2TWFSP8e1nPPS4Lu23qpXfwkw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "+tfsgP5PkTY5mpx7t0rpMVVNXJQMcRkFSsWtnPpntuC/z7I8GS68CBcsHItw5/qbTJxWRY/cLPe/Psxh/SjEkA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.bg": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "AFZc1q0s9zFmxlaBYkZLlXWIAy6uxznU424g5RuNllAjuRKIjSZ7VSF/9yRC4PasVZf1hYiqL5M9oQLXz10yqA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "xBQMDuLOjmj1U0h39tv7B39vmVZXBbRUcy3xkHBKHAyuRoxskDJ3F1LUOOw1/E41+dGQziAe2I0EVukwvQ/h0g==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.bn-BD": {
|
||||
"Humanizer.Core.bn": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "nkgVbmYNo9G8brbR0EmVxsN1nF2eqn+GSCMNY3+Nr2VnEo6RlESd8ZYz6+Q93RpelVq2jmsmXWgnp+TLa43eQA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "U5Li886U/hPdhMxyN9P3eF5dlB15sT/w4F3NOioXsaTQxnJKdxUKglRHiGbakX+aSrsFGok1MoQwVcH7Sac1Lg==",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "ERRbgPC2/Jpj0sSoKFS28dpluDwNteHtmaH2NHq6d9xTbNSNMnINKBR+osDhpFJboJA5EsSEj4ZZwnNDMSnrUw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "kaG0//ULYkyB8Rk4g5PgC2o8jrmG2joc8lgtyDzCiFCaobr0jgUn393JAIB0JUygbngEIfA0PMK3fLbMXcBNLQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.da": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "cf6/r5QJ74pQZVNGCr+P0jXJ7WtP8Wqvur053Rwc+Sfvuj+W3Y7A+Bkwnz1kreGhaH5ikz/7tirnKIkdA119cQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "/MCePoHsSaRo03I1NSRut5iUzhYRIKL+UncbtpN8AZ3Vl0TOu0Psi9/6QyjD4gLLx45puiXB2S0lUjCdYmlP7w==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.de": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "XdinGY9a+vyZ9HOgqTzH+vs0oFSe8bncw+QlbT9N5dyx9ssTnmo94DhSi9+lAK6TONrv9gFJJdLKLgi2xtvdhw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "UoJk/DmAOFZivuMulLoB52Pn5iFvSqurF7qQQAyUAz1/IUZjxkoeoiI5mMILEjVeGoQJqrylt6O+f8johCo0Bg==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.el": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "wib7nnI+RS8WapMRUOXxV7tToWwfm6mjytsZ9xIRDlMk+kN1/YgBdKU+A/gPrKsYIBPzHbCtP8PgrdUm1YsnYA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "5oIHs25LMm1IBrhcxOYYLTFHZQfBaqgKxm0jAkYmsxedpDZkv1k+BHs2D5Kg1672/ZhTs4jsLbqYNXkHm671vw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.es": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "OBQm6ONiwNgPT1FXYHAdEwgEh/F/C3zu99v/SQVxgUYOP0MxCIjt9heizUOAjkB7iuV7mwC8Ni0TNzDt/CXplQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "epxhWWQJ/I610mJGTfQPTocjp7/xCIH6tiREVIlHb/sA1kDAD1zHQcHI+HgWFzn/LzzbVg99y8HVFFHUvjnfXA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.fa": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "f1mSmnmTZhEx10qHiYzX7te6cXlF55TwJDrA1bXnIwe5/pEXBYVp9xvQD76zOEOER57HRNaCwum6ZHNkwwquHw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "9BEFCpbWoUdq2JnLbkohqBfpjjdeMqFhkhCNk3qYmutZwSxuZHrCozppY14SSWI6b6qD1Veb3rVyHGz9Fex5AA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.fi-FI": {
|
||||
"Humanizer.Core.fi": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "M4rtABPYIbJZI41fNbIwY18ly3uwi04pelqG4Ox5PChmDo1KFYeTI+MjqCC3J16CDtV9IYq43JYmz2aS9KkzxQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "9Nbbxs1WZH2Li6Q28tZC06Y0mXwmIGnCEO/2nRzHvTqf07153frndezl5ads4uX8mNs+hMnUGUsr03IsFhI3jQ==",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "Zf6DKTXvKUQ4751UuWdpgLvF0sHoWSdU2nYLd0NRkjxkko+GA1VCo3z6jHJKRNFYrOPXYRfl6e1RqXEEnLmhAw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "k+STDkiJVeO/nA5s0/GeBdwZI17D9KgqyYA4XGxfGxtFACwbFyVKQLWLzlIR2BsrT53EkyKLAzuFJ9FCT2CyfQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
}
|
||||
},
|
||||
"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": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.he": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "2EkoQaHlsPYNOvfGwioQfa5PB5tYCuRI2sbVKDd71SujUUuFmvQo7Y1cqrXkB0aRw4IXRMV4kqVw7WC54vQjIw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "4WIzAg5hocnbnzaTnueYW/BRhCrQoPg/0U6WiUC4BJKGJDuVYbqIa8iVcHqhr7vVKd5ngFbhSsXuCgkYqOMaXQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.hr": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "5eKjP6mxl5F7CmD4++u2aRfPkm7Ky1jdC322lOhORXIMY2QUG7nSk6wdP3n4gZX/35GI3ZbCs2SOA67rc8QGiw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "USin9T/FsHjmxvaolEXgL6FOdw+1Juh7geqcjTvJRCdoo/U4Y/usoAiZRbFtDA//1od4yqSxEbmj+F16nKwbkQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.hu": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "Hfyn9yfC4SkQ2IDV2x74oQauosvbXJdg43bzA/W3uPP3gBBKeZHsGnYdZtlWoXcsH9bbBj6PEi2PQwrHI5Euzg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "Umon7VBTVDs8QFDqNzGm6MBD+43UZJoN44htW/+iJUDF2gl8K8meRup27Dkl2zvewW3ynZic/xwP2hTDaPjIjw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.hy": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "noClOyB3R9YfDQ3R8rMFLXTBlb9qwz6oEnDwgOojv+gUQ6uN+39DNEB3NiTAnL9hryC4ArnCbixOjCXghjIr4Q==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "PzdcqQMwyDYO81PYbzBf+gFMj+wzOYF3kz3mlJCcBX5mSmNRpLIwB2s6PC9OiNN/Y/ya1orHV/z0OY9j7NFc5Q==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.id": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "6uxBc4LeJxm2UWGjkF4BbXYeaNmy6oM8OVaaDmcMtWWanJNpxQ0q7i0UssuLKeFEOhm2KnDVmTcHqkOS3p26wA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "rMb9jvglGeeUxb6Z4EkXTg+et1Ri3DWZjlXs2WFle1Qpu39zWWF0l9rh9PQJb0fzeiZ8jOjiUhUI9lRnoAHHCQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.is": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "DH6j66s1l2OX0rXM22uaFDN6bwh6Ss6LBckaUWUb4t1aEzDIgW09mzZoMG+3zx/LhRibmsur86rf/jN3+2weCQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "xnNgWY9azqXyclRYmy3D6kgnbdCcPFBAEN4yNdhu2IN10p7Y2T9YUw5R9jLWo5+NGM2uzbbyTxPGf4+9YYUc1w==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.it": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "WzxjrUGtRWrB623K8M0wW7qmAdfLroind4yEvsPTRnDFa0prFHLFZxOWF9ypBu6yLXR3T2TdT4Z3r7jYsT+bLA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "iCrKxOX6q/PZgEk3vYyU1fHC0jnq0hiq7nwWXiuV7uF5VypOxwVeao3vkE6MLoNKW7RQN/TbP06n1TOqyhTA7A==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.ja": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "6aHU1JB1gF0azIodYsPFJ5s/5fiDoDiCg5kTI7O+SoOyJrpjKJ8umaMN6r/VwhEUPW1yHxFsEkX/XHO+mXrvAA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "UicfcXwMibwWN3jLesdk702IRQVndgBUqueFr3iXVEn2POue06pHInSIruxNaPTscaNdGKxXZiIiJrN7pCVO9Q==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.ko-KR": {
|
||||
"Humanizer.Core.ko": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "gcYmdBIZ+oBFRTnDZ3RMx4DIOoLcaTa5XPbmefl0GdDJeG7xsFxZQHeRh5sHXNIiMchSYjkPgCgfJTuiwHumIQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "Zr/GwEilhAcmeiDxVKqr79ZK/SEpHfhKzXD15JZ1NpsfYnIfNAN/9i0o535UaadaDDT6EIVpkoGIYAGuYQ0Z4g==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.ku": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "4tMhhNYFOEljlp+y5C/xMVpXga5rGUiYv1gEQJC82tRMhxm/q5elVMQEGtx/iJWZSNiCgj4VZ2Ur4Sqh186gJQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "j5Qm+CY+c8onfjO1O+TReCDg/epCYdfv5N5LNB+Ydcmn06gkR4HvbWJnk5UFLVHz8NMPmtCrv7CYwmPJ0+lJqw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.lb": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "QAObL6iaSztyoYnA8yZlrGraXKXGcxPufRpDbn7S3+boTXY3tkdK4ytZH61AB6wJvuN2ZVqphOMYgCnttO8DUg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "5lpkw49T6MQ7Gwa0k2JV4cw6aH5FdT1Ku1nfGeey/iScajHDV9mvdxjkxRZpFAhJ5W/5yCtMlHH992LpXFJkeA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.lt": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "ePE7K9tngIOLwbKf6lQjtRhTTpiRUvy/DxnHAXI282RDA9DiUB7OE4Zr6NHJeIdrJA4JGat+HosJDPwDp7vV7A==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "1OO4XZ/74tt5nkxYHnpj+ti1w0HdeMGi+AHR59asxcJ3A/iTDZR4RNRIvoj1MdYRe0HdTUjtnGXfA7tXXOkiYg==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.lv": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "HL/3ilL1doE8MublQyMFB4ro/C6M7miAfHvtOXUtc7wsbHJPc9HnbIcBhnyxh3JmYgKbThWoZ6ssfmDeV73qbg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "S4RN1q/saAQxlFlRRUnmi3Oz+mIXRqWtIRZJA3VrvMmBy12cXIdpeY0+d+HCA58WXpI2diOrWxHq9KPUDPunIw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.ms-MY": {
|
||||
"Humanizer.Core.ms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "CuUek2ZvjaR3l4zSsb0lhUujTvtExLkSgP1RlQiaux0RB9Kbh88odwLTDaPhWYcA7D0h0nCkBxSZNwxhP4kWdg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "m0+BJa9vG0GeuAWhmdmMxdUUa1cfGjyHZQeJoeivqxJrrtdUUFekXH0xbpvI3ErC8qu5Ufpmfnr6QAX0WQfkIw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.mt": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "ss4Uxs9mjzaBw5Fhy68ttAP3MeyxHD1Rbk+bLERBMWtwQibER1fcH4JYu/EuRJ7yDe4oObOjRqu3GzNrXGLbXg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "8GfXTko/iweepZHcUcOANreClfvTeCDfSuiMxLtFbUZQOBegLZ6VnTVun2gVtv5OANdOk64qhqUKIVLq2rG0rw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.nb": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "o579CpW97Oj8Xp4IgTzlwieH+QOV/gRbEXUKapaQdiK/bOCOMm9N1z+wNhbDYmY/Uym6K34PG7xpbqio0Nj9QQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "UcdV9TWXG2MFVEkqHy8oD2ZbuFhFPLY7ScCJKWBu7OsgInyuHZe72hHP/i52T0FAgiVbrB+Urd3uoWiUT7hP9Q==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
}
|
||||
},
|
||||
"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": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.nl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "FmFCjnc53MH5tk6sRgOB/pSjFUmCiPBO2OcdV1szqYPJ0dGNkGw3pHUfE3P3gAHYuHL/GdYyhCi74/BiyK5C2g==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "b+Y1CFId1ucl33OvNITID0eEVesOKiw/dmrcDJ4Z/P9KEuMTeE8aKu5/iSjwtPVV+SagaMx9XzZe7kVYyRqhuQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.pl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "JMUCLMrAEzRR9b+4BY4+b8D+ymQIUAp74b7+zWY/ZLOnwc8Yw01sFx51hbJCgohEVXAitOAyHjzvrhUoijG32Q==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "LJCt3+dGUQhopQ/1w/zzBNlUHtjkdrt57zS+llMQeFm+1u0e3R+lWJx4zP+hqZcpRA7armR38KghvRSz+bYBbg==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.pt": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "Xb9XhdAQKG9qkbR/SPO5oYPiUtDaueTwJPRnUoBBIbdDTzpQlSUVjJ/DFez+/X8d8GsANWjof3g/5YnOWZ16+g==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "+cggEy6W08xD8GU45/EaMfvNQMCd7K1G7hprSPty/9zIoM3OzaajdMA/Oxrb0mHIKpARxCNbQ29ltOtip3MTNA==",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "SSaZzeQmrbmDh2/CHSUF3jbU2V0ilcVHPa32/i7g0TXeywzP35CIcZ7vfKlJcfLSnempMg1bdm703hfeUdoLTw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "JwdY+SywNIDrFcWKyiZsgsSJkIleoAXAtCLYiLn+CNCk+135VwhchZHm5B9nNvpLjGVaP5Y0ouZQ8xTx1oeZag==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.ru": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "+uTlQsIs4blTP6f0mnQ7lnn8wyOLmZFMWHqqTh3+Ul960FLlo0UnxYScV1sgNLHudv/04IiLsuj3bGxWJKW0FQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "1vXr7pkV0zSZF6ENJUadsAagubrBAvTPHPgdgttHlMhUQ1Zz9ZWTJaQdmf2RCen4YywMmfsQQZiGqJl52LVT8Q==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.sk": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "dcSN1GYFUNTQFlLljGZLarLe1e258wtNK+k0Mj2sghN+ffvlK1aRhUvanmqAaURfkK5ume1Q4e0A4/ATMNNFsw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "LD93nJw1zwXuC+LSw3+02Jk02bdAoENHzb/qrMoGBf70XeV2rBeoxSyJBVLHQ00RZfwNSy6TCTXQyagESVmN+Q==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.sl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "H05Tl8RQs8RoJ+UWYPga087x4Be57EpRk6iYwo4absTlFyPeISv8whUqayJBvDpNGS94QHennrnfembrCnhJXg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "utURJ8K/tnbA5KXlKcT8T+35MEVdZrXUvu2tH+T6M9xxJ1MhLGuFWnc687pcsLdpaLnGB7ZVwrAITlueAE+KGA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.sr": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "FLH10qbsgL5NJJ/moB1iSnmO1wkVDloJYwq1CwIKp0PePb4bViO0z4nTXKCERWn3zl3WXeCJ5gOUyrvplr6bSQ==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "e2bVzUAjEpxjFdLVmXZL9EsdVojlV/8GMwZzwDS6OByfG+PP5JxemKylK6FoUK5X2IBPNg68LOvn72WgwaruIQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.sr-Latn": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "aTIigE/P5ahO5RO88igcz5V8Fo0MP8+Cu/NFVFXQJv+Yw39dQ1fOscLpVXenOg22sy2Tz2pi/JNfK42SDEiV6Q==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "VXPlm+eg5sraDhZ50WGiMZMvj4I2d+0VAy8cNaLLXNt+EgkUz646dRfg5HrQPPjtBbegj6o7gcg8Xl6z0ke+hA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.sv": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "k3URfcuKrOfL64Eg5mE2uV8YotKnhObFsJaIiyGhRPyl2QrrPU6VXKM2DmLa6Cfa1RN3zHYLv8PzBo1g6jVBYA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "COq4bbUKk7F1jfE2sC/ZmomoQqX0Herwe1iXZyDl5zyw21mUY9wL96S1qbRmRp15UmGzFJdqBPdnd58P7kVMyg==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.th-TH": {
|
||||
"Humanizer.Core.th": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "9vaBFcdcXvE4uXr4VLbhPQ/Hv7lKf4Cb1HzThrnWklTk6uzZkmYYRs/yat+apk7of0VAlY8rthsMiNIwwSWW+A==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "8A4lZCLE6bEPYdQKeGfPBHDgAsY30TWWdjoWA1+vAfLA9Ip1l2Ons+Ngw4npkcoI3qdCidU6hWOsFnPfJ8ZufQ==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.tr": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "d5xUVe/1NrmAeBTL3KamGtZJ/3ja7O8OOWV/ICmNeOEHPMg5wZTWnQ3DOOrJAdrnf5D6kNcngKxYwKZYoRS19g==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "aRAxq1VyuIVoFdx8TZyVsRoDk0yqe9NATjhGgWHPsdR8TVHnuiyXeEbiXf846870VteyLJL9tVy3jaj2rC45tw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.uk": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "3i9xEIoqoWUtNKaihYqr9EfQw7ilWiDIaDwrXDrADldO1HHFAcc+F2vNEB1sZ81/F/1FBCthyODTFDBD1Ma4Kg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "NPnGZ1jdoMDYGrsHt5aLlj7Ambp7i5ER/5BbtjBSegn3+PliMbd4Eftr1Ind4qvCU6rTP2a9lUqBoRd+hSVA+g==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.uz-Cyrl-UZ": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "LZK6cDgU0fQ0zURLYwk4qlodc0ZGcXkxV6zfHJbHXBsow12cJ2p7NhL495ihWLnDrHfD20+J1y9rTvC9vIWnFA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "8NSyuiTlCfse1ki9QeO35IpZLcxoMlSMGvEY892aw9CNGaDMROQjFzf1iKJkWTyB9atrHHPuWlasBAFk2QkT9w==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.uz-Latn-UZ": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "AmoGiYjAjoi7htH6Vimd266rGRfyU0Jsa9BjALP7EAsWILjAzba3aMmChK33A6apFzC9fv2XT0amZsgIqBvR/Q==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "FaSETkwfUiDU/RHlD54xbBKEZXnFIUnTNTnA5ZYXgDOiMmX6cY7vyRDmI1YpQtO+QR1irSettpLFpHeysslnpA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.vi": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "PZnFdIJF54ODyMazZDSe30NuRuVx4/c/3KBXzwFX3tR+yz+gJkYlYpTI8ozRLF2/mlcFr9iFAU4Pau6jJFevFw==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "Uz5gph//0eNP0Ta1tqdKDyghiUnZdW0aVW4MK/poD5hLEH1qLoDTWuTb8gxj3F8GvSFjgVmF9HZGQx5cO06N8Q==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.zh-CN": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "GX8JvHC9/ghN8ZU/MadCnRrLB6kJXQEuJz3i0hS03VVrTZlHMRQfXEC3NhSktSJ9FCyYq+WZaw7jhmSaQ/S2pA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "7veG4JdZ4OeYNlgci/6I/Xf9G/iCljBcwsbLVyGaqdia+2UMAp1iGu+MCs3zKOmEvcb4ipHGFkwT5d/2WfEevA==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.zh-Hans": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "S0nzvET+5M188jPN2cUiMguIszKZQ6tVyvAem3xBjvF0LjodAlAmZNlMjw5zaPMACuWMkOgvpKjuA8dhFa2Gqg==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "z8E2R0QL43NB6m8ntQaL1cONMjudoIJXvMQUFXG4OaKp5iyYqWs6FMC2B7x2rLxOynpsxoq5AQ/8BO0pMdfZrw==",
|
||||
"dependencies": {
|
||||
"Humanizer.Core": "[3.0.0-beta.96]"
|
||||
"Humanizer.Core": "[3.0.10]"
|
||||
}
|
||||
},
|
||||
"Humanizer.Core.zh-Hant": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.0-beta.96",
|
||||
"contentHash": "nzWjsEb7JLHtBOuWuKBI1b0rm31C4Q8DSJHZy6UPhjO1HVra7GcMVeNyxXsljaVtdjR5kjLaPuhrr0yDNEUtYA==",
|
||||
"resolved": "3.0.10",
|
||||
"contentHash": "KI5Nv82v9diPBxcBAwIAvZQHjSrDy7He0atnzdZghS+QchHwlkPKhTyEzVSbOZdD42RxCX1ArMiL864A2iw34Q==",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "13.0.1",
|
||||
"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": {
|
||||
"type": "Project"
|
||||
},
|
||||
@@ -714,14 +541,11 @@
|
||||
"dependencies": {
|
||||
"CaddyManager.Contracts": "[1.0.0, )",
|
||||
"Docker.DotNet": "[3.125.15, )",
|
||||
"Humanizer": "[3.0.0-beta.96, )",
|
||||
"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, )",
|
||||
"Humanizer": "[3.0.10, )",
|
||||
"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
|
||||
# caddy container's config volume. Defaults to /etc/caddy/Caddyfile, so this line is optional above.
|
||||
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
|
||||
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:
|
||||
- "8080:8080"
|
||||
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)
|
||||
- Restart caddy container on demand
|
||||
- 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>
|
||||
|
||||
|
||||
@@ -18,7 +18,13 @@
|
||||
ASPNETCORE_ENVIRONMENT: "Production"
|
||||
CaddyService__ConfigDir: "/config"
|
||||
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"
|
||||
# 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:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
|
||||
@@ -18,5 +18,5 @@ exec docker run --rm \
|
||||
--volume "$repo_root":/src \
|
||||
--workdir /src \
|
||||
--volume caddymanager-nuget:/root/.nuget/packages \
|
||||
mcr.microsoft.com/dotnet/sdk:9.0 \
|
||||
mcr.microsoft.com/dotnet/sdk:10.0 \
|
||||
dotnet "$@"
|
||||
|
||||
Reference in New Issue
Block a user