mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-04 04:18:13 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae556168e | ||
|
|
b44dde2c77 | ||
|
|
23bf6bb9e0 | ||
|
|
dfb0b94e4a |
@@ -477,9 +477,9 @@ sudo ./scripts/install.sh --uninstall --purge --system
|
||||
|
||||
**Codebase overview:**
|
||||
|
||||
- **Entry and config:** `Program.cs` wires the host; configuration (e.g. `Dnsmasq__*`) is in `Configuration/` (`DnsmasqOptions`, `ApplicationOptions`). Options are validated at startup.
|
||||
- **Config and hosts:** Services in `Services/` and `Services/Abstractions/` read and write dnsmasq config and hosts. A **managed** config file and optional managed hosts file are written by the app; the main config must include the managed file (e.g. via a final `conf-file=` line). Caches (`ConfigSetCache`, `HostsCache`) keep in-memory snapshots and refresh on file changes or staleness.
|
||||
- **Parsers and models:** `Parsers/` parses dnsmasq config lines and files (`DnsmasqConfIncludeParser`, `DnsmasqConfDirectiveParser`, etc.). Models in `Models/` represent config sets, config, sources, DHCP entries, hosts, leases.
|
||||
- **Entry and config:** `Program.cs` wires the host; configuration (e.g. `Dnsmasq__*`) lives in `Models/Config/` (`DnsmasqOptions`, `ApplicationOptions`, `DnsmasqOptionsValidator`). Options are validated at startup.
|
||||
- **Infrastructure:** Under `Infrastructure/`: **Client/** (HTTP API clients and `Abstractions/`), **Services/** and **Services/Abstractions/** (caches, config/hosts/leases services, reload, hosted services), **Parsers/** (dnsmasq config and hosts line parsers), **Helpers/Config/** and **Helpers/Http/** (option keys, encoding, tooltips, JSON options). A **managed** config file and optional managed hosts file are written by the app; the main config must include the managed file (e.g. via a final `conf-file=` line). Caches (`ConfigSetCache`, `HostsCache`) keep in-memory snapshots and refresh on file changes or staleness.
|
||||
- **Models:** `Models/Config/` (options, validators, `DnsmasqConfLine`); `Models/Contracts/` (snapshots and DTOs such as `ConfigSetSnapshot`, `HostsSnapshot`, `ManagedConfigContent`, `ProcessRunResult`, `ReloadResult`); `Models/Dnsmasq/` (status, `SaveWithReloadResult`, and `EffectiveConfig/` for effective config and sources); `Models/Client/`, `Models/Dhcp/`, `Models/Hosts/` (UI and API DTOs).
|
||||
- **API and UI:** `Controllers/` expose API endpoints; `Components/` contains Blazor Server components for config editor, hosts, DHCP, Dnsmasq status, and app settings. Static assets in `wwwroot/`.
|
||||
- **Tests:** `DnsmasqWebUI.Tests` contains unit tests for parsers, config set service, config, and related logic. Run with `dotnet test`.
|
||||
|
||||
|
||||
+19
-11
@@ -100,15 +100,21 @@ trim_repo() {
|
||||
collect_config() {
|
||||
CONFIG_VARS_FILE="$(mktemp)"
|
||||
export CONFIG_VARS_FILE
|
||||
# Env vars: strip DNSMASQ_WEBUI_ prefix; key=value per line
|
||||
env | grep '^DNSMASQ_WEBUI_' | while IFS= read -r line; do
|
||||
key="${line%%=*}"
|
||||
key="${key#DNSMASQ_WEBUI_}"
|
||||
value="${line#*=}"
|
||||
printf '%s=%s\n' "$key" "$value" >> "$CONFIG_VARS_FILE"
|
||||
done
|
||||
# Env vars: strip DNSMASQ_WEBUI_ prefix; key=value per line. set +e so grep (no match) / read (EOF) / [ -f ] do not trigger exit.
|
||||
set +e
|
||||
env | grep '^DNSMASQ_WEBUI_' > "${CONFIG_VARS_FILE}.env" 2>/dev/null
|
||||
if [ -f "${CONFIG_VARS_FILE}.env" ] && [ -s "${CONFIG_VARS_FILE}.env" ]; then
|
||||
while IFS= read -r line; do
|
||||
key="${line%%=*}"
|
||||
key="${key#DNSMASQ_WEBUI_}"
|
||||
value="${line#*=}"
|
||||
printf '%s=%s\n' "$key" "$value" >> "$CONFIG_VARS_FILE"
|
||||
done < "${CONFIG_VARS_FILE}.env"
|
||||
fi
|
||||
rm -f "${CONFIG_VARS_FILE}.env"
|
||||
set -e
|
||||
# --set lines (last occurrence of each key wins when systemd reads the file)
|
||||
[ -s "$CONFIG_SET_FILE" ] && cat "$CONFIG_SET_FILE" >> "$CONFIG_VARS_FILE"
|
||||
[ -s "$CONFIG_SET_FILE" ] && cat "$CONFIG_SET_FILE" >> "$CONFIG_VARS_FILE" || true
|
||||
}
|
||||
|
||||
# Write env file for the app/service. $1 = path. Adds ASPNETCORE_URLS=http://0.0.0.0:8080 if not in config (for service).
|
||||
@@ -219,6 +225,7 @@ print_env_detection() {
|
||||
}
|
||||
|
||||
# GET release (latest or by tag). Output raw JSON to stdout.
|
||||
# Use </dev/null so curl does not consume stdin (script when run via curl | sh).
|
||||
fetch_release() {
|
||||
local api_url
|
||||
if [ -z "$RELEASE_TAG" ] || [ "$RELEASE_TAG" = "latest" ]; then
|
||||
@@ -226,7 +233,7 @@ fetch_release() {
|
||||
else
|
||||
api_url="https://api.github.com/repos/$GITHUB_REPO/releases/tags/$RELEASE_TAG"
|
||||
fi
|
||||
resp="$(curl -sSL -A "dnsmasq-webui-install/1.0" -w "\n%{http_code}" "$api_url")"
|
||||
resp="$(curl -sSL -A "dnsmasq-webui-install/1.0" -w "\n%{http_code}" "$api_url" </dev/null)"
|
||||
code="$(echo "$resp" | tail -n1)"
|
||||
body="$(echo "$resp" | sed '$d')"
|
||||
if [ "$code" != "200" ]; then
|
||||
@@ -242,7 +249,7 @@ list_releases() {
|
||||
check_jq
|
||||
detect_repo
|
||||
api_url="https://api.github.com/repos/$GITHUB_REPO/releases?per_page=20"
|
||||
resp="$(curl -sSL -A "dnsmasq-webui-install/1.0" -w "\n%{http_code}" "$api_url")"
|
||||
resp="$(curl -sSL -A "dnsmasq-webui-install/1.0" -w "\n%{http_code}" "$api_url" </dev/null)"
|
||||
code="$(echo "$resp" | tail -n1)"
|
||||
body="$(echo "$resp" | sed '$d')"
|
||||
if [ "$code" != "200" ]; then
|
||||
@@ -440,6 +447,7 @@ do_install() {
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Installing dnsmasq-webui..."
|
||||
check_jq
|
||||
detect_repo
|
||||
|
||||
@@ -481,7 +489,7 @@ do_install() {
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
tmpzip="${TMPDIR:-/tmp}/dnsmasq-webui-$rid.zip"
|
||||
echo "Downloading $url ..."
|
||||
curl -sSL -A "dnsmasq-webui-install/1.0" -o "$tmpzip" "$url"
|
||||
curl -sSL -A "dnsmasq-webui-install/1.0" -o "$tmpzip" "$url" </dev/null
|
||||
echo "Extracting to $INSTALL_DIR ..."
|
||||
unzip -o -q "$tmpzip" -d "$INSTALL_DIR"
|
||||
rm -f "$tmpzip"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
@@ -145,14 +147,57 @@ public class DnsmasqConfIncludeParserTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetIncludedPaths_testdata_dnsmasq_conf_ReturnsMainOnlyWhenConfDirMissing()
|
||||
public void GetIncludedPaths_ConfDirMissing_ReturnsMainOnly()
|
||||
{
|
||||
var mainPath = TestDataHelper.GetPath("dnsmasq.conf");
|
||||
if (!File.Exists(mainPath))
|
||||
return; // testdata not copied
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(mainPath);
|
||||
Assert.Single(paths);
|
||||
Assert.Equal(Path.GetFullPath(mainPath), paths[0]);
|
||||
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-confdir-missing-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(baseDir);
|
||||
try
|
||||
{
|
||||
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
||||
File.WriteAllText(main, "conf-dir=nonexistent-subdir\n");
|
||||
var paths = DnsmasqConfIncludeParser.GetIncludedPaths(main);
|
||||
Assert.Single(paths);
|
||||
Assert.Equal(Path.GetFullPath(main), paths[0]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(baseDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mirrors harness layout: main + conf-dir with multiple files (including lan-dns.conf) + conf-file. Asserts all appear in load order with correct sources.</summary>
|
||||
[Fact]
|
||||
public void GetIncludedPathsWithSource_ConfDirAndConfFile_ReturnsAllFilesInLoadOrderWithSources()
|
||||
{
|
||||
var baseDir = Path.Combine(Path.GetTempPath(), "dnsmasq-harness-layout-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(baseDir);
|
||||
var confDir = Path.Combine(baseDir, "dnsmasq.d");
|
||||
Directory.CreateDirectory(confDir);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Path.Combine(confDir, "01-other.conf"), "# other\n");
|
||||
File.WriteAllText(Path.Combine(confDir, "02-servers.conf"), "# servers\n");
|
||||
File.WriteAllText(Path.Combine(confDir, "lan-dns.conf"), "# lan dns\n");
|
||||
File.WriteAllText(Path.Combine(baseDir, "zz-dnsmasq-webui.conf"), "# managed\n");
|
||||
var main = Path.Combine(baseDir, "dnsmasq.conf");
|
||||
File.WriteAllText(main, "conf-dir=dnsmasq.d\nconf-file=zz-dnsmasq-webui.conf\n");
|
||||
var withSource = DnsmasqConfIncludeParser.GetIncludedPathsWithSource(main);
|
||||
Assert.Equal(5, withSource.Count);
|
||||
Assert.Equal(Path.GetFullPath(main), withSource[0].Path);
|
||||
Assert.Equal(DnsmasqConfFileSource.Main, withSource[0].Source);
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine(confDir, "01-other.conf")), withSource[1].Path);
|
||||
Assert.Equal(DnsmasqConfFileSource.ConfDir, withSource[1].Source);
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine(confDir, "02-servers.conf")), withSource[2].Path);
|
||||
Assert.Equal(DnsmasqConfFileSource.ConfDir, withSource[2].Source);
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine(confDir, "lan-dns.conf")), withSource[3].Path);
|
||||
Assert.Equal(DnsmasqConfFileSource.ConfDir, withSource[3].Source);
|
||||
Assert.Equal(Path.GetFullPath(Path.Combine(baseDir, "zz-dnsmasq-webui.conf")), withSource[4].Path);
|
||||
Assert.Equal(DnsmasqConfFileSource.ConfFile, withSource[4].Source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(baseDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Services;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Services;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/config/set.</summary>
|
||||
public interface IConfigSetClient
|
||||
{
|
||||
Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET/PUT api/hosts.</summary>
|
||||
public interface IHostsClient
|
||||
{
|
||||
Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default);
|
||||
Task<IReadOnlyList<ReadOnlyHostsFile>> GetReadOnlyHostsAsync(CancellationToken ct = default);
|
||||
Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for POST api/reload.</summary>
|
||||
public interface IReloadClient
|
||||
{
|
||||
Task<ReloadResult> ReloadAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/status.</summary>
|
||||
public interface IStatusClient
|
||||
{
|
||||
Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
|
||||
public sealed class StatusClient : IStatusClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
/// <summary>Matches API serialization (camelCase) so nested EffectiveConfigSources and tuple (value, source) deserialize correctly.</summary>
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public StatusClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default) =>
|
||||
await _http.GetFromJsonAsync<DnsmasqServiceStatus>("api/status", JsonOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/status.");
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
@* Reusable value-only display for boolean/flag options. Edit mode: checkbox (unless readonly); View: "Enabled" / "Disabled". *@
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
@* Value-only display for the port option. Edit mode: number input (unless readonly); View: text with "(default DNS port)". *@
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Descriptor.GetSource()?.IsReadOnly != true)
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
@using DnsmasqWebUI.Models.Config
|
||||
@using DnsmasqWebUI.Services.Abstractions
|
||||
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
||||
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
|
||||
|
||||
@* Custom fragment renders only the value; we keep the row shell (label + value slot + badge) here for consistency. *@
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
<span class="ec-field-value-item" title="@(Source?.GetReadOnlyTooltip())">
|
||||
@Value
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
<li class="ec-field">
|
||||
<span class="ec-field-label" title="@LabelTooltip"><strong>@Label:</strong></span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
<li class="ec-field" title="@(Source?.GetReadOnlyTooltip())">
|
||||
<span class="ec-field-label" title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></span>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Status
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
@if (Status != null)
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
|
||||
<CascadingValue Value="DisplayMode">
|
||||
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace DnsmasqWebUI.Components.Dnsmasq;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace DnsmasqWebUI.Components.Dnsmasq;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Client.Models;
|
||||
using DnsmasqWebUI.Client.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Client;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -24,9 +24,12 @@ else if (_status != null)
|
||||
<h2 class="page-section-title">Service configuration</h2>
|
||||
<p class="mb-1"><strong>Config files (load order):</strong></p>
|
||||
<ul class="list-unstyled small ms-2 mb-0">
|
||||
@if (_configSet != null && _configSet.Files?.Count > 0)
|
||||
@{
|
||||
var configFileList = _status.ConfigFiles?.Count > 0 ? _status.ConfigFiles : _configSet?.Files;
|
||||
}
|
||||
@if (configFileList != null && configFileList.Count > 0)
|
||||
{
|
||||
@foreach (var f in _configSet.Files)
|
||||
@foreach (var f in configFileList)
|
||||
{
|
||||
var isMain = string.Equals(f.Path, _status.MainConfigPath, StringComparison.Ordinal);
|
||||
var isManaged = f.IsManaged;
|
||||
|
||||
@@ -7,19 +7,20 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using DnsmasqWebUI
|
||||
@using DnsmasqWebUI.Client.Models
|
||||
@using DnsmasqWebUI.Client.Services.Abstractions
|
||||
@using DnsmasqWebUI.Models.Client
|
||||
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
||||
@using DnsmasqWebUI.Components
|
||||
@using DnsmasqWebUI.Components.Modals
|
||||
@using DnsmasqWebUI.Components.Dnsmasq
|
||||
@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Components.Hosts
|
||||
@using DnsmasqWebUI.Infrastructure
|
||||
@using DnsmasqWebUI.Infrastructure.Helpers.Config
|
||||
@using DnsmasqWebUI.Models
|
||||
@using DnsmasqWebUI.Models.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||
@using DnsmasqWebUI.Models.Hosts
|
||||
@using DnsmasqWebUI.Models.Dhcp
|
||||
@using DnsmasqWebUI.Models.Status
|
||||
@using DnsmasqWebUI.Configuration
|
||||
@using DnsmasqWebUI.Client.Http.Abstractions
|
||||
@using DnsmasqWebUI.Services.Abstractions
|
||||
@using DnsmasqWebUI.Models.Config
|
||||
@using DnsmasqWebUI.Infrastructure.Client.Abstractions
|
||||
@using Microsoft.Extensions.Options
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DnsmasqWebUI.Controllers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DnsmasqWebUI.Controllers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DnsmasqWebUI.Controllers;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DnsmasqWebUI.Controllers;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DnsmasqWebUI.Controllers;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -76,6 +77,7 @@ public class StatusController : ControllerBase
|
||||
ManagedFilePathExists: !string.IsNullOrEmpty(set.ManagedFilePath) && System.IO.File.Exists(set.ManagedFilePath),
|
||||
LeasesPathConfigured: !string.IsNullOrEmpty(effectiveLeasesPath),
|
||||
LeasesPathExists: !string.IsNullOrEmpty(effectiveLeasesPath) && System.IO.File.Exists(effectiveLeasesPath),
|
||||
ConfigFiles: set.Files.Count > 0 ? set.Files : null,
|
||||
ReloadCommandConfigured: !string.IsNullOrWhiteSpace(_options.ReloadCommand),
|
||||
StatusCommandConfigured: !string.IsNullOrWhiteSpace(_options.StatusCommand),
|
||||
StatusShowConfigured: !string.IsNullOrWhiteSpace(_options.StatusShowCommand),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Reflection;
|
||||
using DnsmasqWebUI.Client.Http;
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Client;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/config/set.</summary>
|
||||
public interface IConfigSetClient
|
||||
{
|
||||
/// <summary>Gets the config set (main + conf-file/conf-dir) and managed file path from GET api/config/set.</summary>
|
||||
Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default);
|
||||
}
|
||||
+6
-2
@@ -1,11 +1,15 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET/PUT api/dhcp/hosts.</summary>
|
||||
public interface IDhcpHostsClient
|
||||
{
|
||||
/// <summary>Gets DHCP host entries from GET api/dhcp/hosts.</summary>
|
||||
Task<IReadOnlyList<DhcpHostEntry>> GetDhcpHostsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Writes DHCP host entries and triggers reload via PUT api/dhcp/hosts.</summary>
|
||||
Task<SaveWithReloadResult> SaveDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET/PUT api/hosts.</summary>
|
||||
public interface IHostsClient
|
||||
{
|
||||
/// <summary>Gets managed hosts file entries from GET api/hosts.</summary>
|
||||
Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Gets read-only hosts files (system + addn-hosts) from GET api/hosts/readonly.</summary>
|
||||
Task<IReadOnlyList<ReadOnlyHostsFile>> GetReadOnlyHostsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Writes managed hosts file and triggers reload via PUT api/hosts.</summary>
|
||||
Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default);
|
||||
}
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/leases.</summary>
|
||||
public interface ILeasesClient
|
||||
{
|
||||
/// <summary>Gets DHCP lease entries from GET api/leases. Optionally forces a cache refresh on the server.</summary>
|
||||
/// <param name="forceRefresh">When true, invalidates the server cache so the next read is from disk (e.g. after manual Refresh).</param>
|
||||
Task<LeasesResult> GetLeasesAsync(bool forceRefresh = false, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
|
||||
/// <summary>Typed client for POST api/reload.</summary>
|
||||
public interface IReloadClient
|
||||
{
|
||||
/// <summary>Triggers dnsmasq reload via POST api/reload.</summary>
|
||||
Task<ReloadResult> ReloadAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
|
||||
/// <summary>Typed client for GET api/status.</summary>
|
||||
public interface IStatusClient
|
||||
{
|
||||
/// <summary>Gets dnsmasq and config status from GET api/status.</summary>
|
||||
Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default);
|
||||
}
|
||||
+6
-4
@@ -1,8 +1,10 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
public sealed class ConfigSetClient : IConfigSetClient
|
||||
{
|
||||
@@ -11,6 +13,6 @@ public sealed class ConfigSetClient : IConfigSetClient
|
||||
public ConfigSetClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<DnsmasqConfigSet> GetConfigSetAsync(CancellationToken ct = default) =>
|
||||
await _http.GetFromJsonAsync<DnsmasqConfigSet>("api/config/set", ct)
|
||||
await _http.GetFromJsonAsync<DnsmasqConfigSet>("api/config/set", ApiJsonOptions.ClientOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/config/set.");
|
||||
}
|
||||
+9
-7
@@ -1,10 +1,12 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
public sealed class DhcpHostsClient : IDhcpHostsClient
|
||||
{
|
||||
@@ -14,13 +16,13 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
|
||||
|
||||
public async Task<IReadOnlyList<DhcpHostEntry>> GetDhcpHostsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var list = await _http.GetFromJsonAsync<List<DhcpHostEntry>>("api/dhcp/hosts", ct);
|
||||
var list = await _http.GetFromJsonAsync<List<DhcpHostEntry>>("api/dhcp/hosts", ApiJsonOptions.ClientOptions, ct);
|
||||
return list ?? new List<DhcpHostEntry>();
|
||||
}
|
||||
|
||||
public async Task<SaveWithReloadResult> SaveDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ct);
|
||||
var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ApiJsonOptions.ClientOptions, ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
@@ -29,7 +31,7 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
|
||||
{
|
||||
try
|
||||
{
|
||||
var err = System.Text.Json.JsonSerializer.Deserialize<JsonError>(body);
|
||||
var err = System.Text.Json.JsonSerializer.Deserialize<JsonError>(body, ApiJsonOptions.ClientOptions);
|
||||
if (!string.IsNullOrEmpty(err?.Error))
|
||||
msg = err.Error;
|
||||
}
|
||||
@@ -37,7 +39,7 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
|
||||
}
|
||||
throw new HttpRequestException(msg);
|
||||
}
|
||||
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ct)
|
||||
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ApiJsonOptions.ClientOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/dhcp/hosts.");
|
||||
}
|
||||
|
||||
+9
-7
@@ -1,9 +1,11 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
public sealed class HostsClient : IHostsClient
|
||||
{
|
||||
@@ -13,21 +15,21 @@ public sealed class HostsClient : IHostsClient
|
||||
|
||||
public async Task<IReadOnlyList<HostEntry>> GetHostsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var list = await _http.GetFromJsonAsync<List<HostEntry>>("api/hosts", ct);
|
||||
var list = await _http.GetFromJsonAsync<List<HostEntry>>("api/hosts", ApiJsonOptions.ClientOptions, ct);
|
||||
return list ?? new List<HostEntry>();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ReadOnlyHostsFile>> GetReadOnlyHostsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var list = await _http.GetFromJsonAsync<List<ReadOnlyHostsFile>>("api/hosts/readonly", ct);
|
||||
var list = await _http.GetFromJsonAsync<List<ReadOnlyHostsFile>>("api/hosts/readonly", ApiJsonOptions.ClientOptions, ct);
|
||||
return list ?? new List<ReadOnlyHostsFile>();
|
||||
}
|
||||
|
||||
public async Task<SaveWithReloadResult> SaveHostsAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default)
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync("api/hosts", entries, ct);
|
||||
var response = await _http.PutAsJsonAsync("api/hosts", entries, ApiJsonOptions.ClientOptions, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ct)
|
||||
return await response.Content.ReadFromJsonAsync<SaveWithReloadResult>(ApiJsonOptions.ClientOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/hosts.");
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -1,8 +1,9 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
public sealed class LeasesClient : ILeasesClient
|
||||
{
|
||||
@@ -13,7 +14,7 @@ public sealed class LeasesClient : ILeasesClient
|
||||
public async Task<LeasesResult> GetLeasesAsync(bool forceRefresh = false, CancellationToken ct = default)
|
||||
{
|
||||
var url = forceRefresh ? "api/leases?refresh=true" : "api/leases";
|
||||
return await _http.GetFromJsonAsync<LeasesResult>(url, ct)
|
||||
return await _http.GetFromJsonAsync<LeasesResult>(url, ApiJsonOptions.ClientOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/leases.");
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,8 +1,9 @@
|
||||
using DnsmasqWebUI.Client.Http.Abstractions;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
public sealed class ReloadClient : IReloadClient
|
||||
{
|
||||
@@ -14,7 +15,7 @@ public sealed class ReloadClient : IReloadClient
|
||||
{
|
||||
var response = await _http.PostAsync("api/reload", null, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ReloadResult>(ct)
|
||||
return await response.Content.ReadFromJsonAsync<ReloadResult>(ApiJsonOptions.ClientOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/reload.");
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using System.Net;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Http;
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
/// <summary>
|
||||
/// DelegatingHandler that rewrites relative request URIs to the current request's scheme, host, and path base.
|
||||
@@ -0,0 +1,18 @@
|
||||
using DnsmasqWebUI.Infrastructure.Client.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Client;
|
||||
|
||||
public sealed class StatusClient : IStatusClient
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public StatusClient(HttpClient http) => _http = http;
|
||||
|
||||
public async Task<DnsmasqServiceStatus> GetStatusAsync(CancellationToken ct = default) =>
|
||||
await _http.GetFromJsonAsync<DnsmasqServiceStatus>("api/status", ApiJsonOptions.ClientOptions, ct)
|
||||
?? throw new InvalidOperationException("Unexpected null from api/status.");
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Text;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure;
|
||||
|
||||
/// <summary>Encoding for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.</summary>
|
||||
public static class DnsmasqFileEncoding
|
||||
{
|
||||
public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Literal dnsmasq .conf option names (config keys). Use these everywhere we reference option names
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Text;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
|
||||
/// <summary>Encoding and reading helpers for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.</summary>
|
||||
public static class DnsmasqFileEncoding
|
||||
{
|
||||
private const char Utf8BomChar = '\uFEFF';
|
||||
|
||||
public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
/// <summary>Read config file as UTF-8 and strip UTF-8 BOM from first line if present, so conf-dir= etc. are recognized.</summary>
|
||||
public static string[] ReadConfigLines(string path)
|
||||
{
|
||||
var lines = File.ReadAllLines(path, Encoding.UTF8);
|
||||
StripBomFromFirstLine(lines);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>Strip UTF-8 BOM from first line if present (mutates list). Use so we do not perpetuate BOM when writing.</summary>
|
||||
public static void StripBomFromFirstLine(IList<string> lines)
|
||||
{
|
||||
if (lines.Count == 0) return;
|
||||
var first = lines[0];
|
||||
if (first.Length > 0 && first[0] == Utf8BomChar)
|
||||
lines[0] = first[1..];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Frozen;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Short explainer tooltips for dnsmasq option names. Shown on hover over the field label.
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Status;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Models.EffectiveConfig;
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Builds field descriptors for viewing and editing. Mapping is done once here via delegates passed to each descriptor.
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Http;
|
||||
|
||||
/// <summary>
|
||||
/// Shared JSON options for API serialization (Controllers) and client deserialization (HTTP clients).
|
||||
/// Keeps camelCase and enum-as-string in one place so server and client stay in sync.
|
||||
/// </summary>
|
||||
public static class ApiJsonOptions
|
||||
{
|
||||
/// <summary>Options for Controllers (AddJsonOptions). Apply via ConfigureServer(JsonSerializerOptions).</summary>
|
||||
public static void ConfigureServer(JsonSerializerOptions options)
|
||||
{
|
||||
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
}
|
||||
|
||||
/// <summary>Options for HTTP clients when deserializing API responses (camelCase, case-insensitive, enum as string).</summary>
|
||||
public static JsonSerializerOptions ClientOptions { get; } = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Shared Superpower text-parser helpers used by .conf and related parsers.
|
||||
+1
-1
@@ -3,7 +3,7 @@ using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a single <c>dhcp-host=</c> line from a dnsmasq .conf file. Same format as long option without <c>--</c>.
|
||||
+1
-1
@@ -2,7 +2,7 @@ using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Key/value parsing for dnsmasq .conf lines. Used by <see cref="DnsmasqConfIncludeParser"/> to build effective config.
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a dnsmasq .conf file line-by-line into <see cref="DnsmasqConfLine"/> variants (BlankLine, CommentLine, DhcpHostLine, OtherLine).
|
||||
+8
-5
@@ -1,10 +1,11 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the main dnsmasq .conf file only for <c>conf-file=</c> and <c>conf-dir=</c> directives
|
||||
@@ -21,6 +22,7 @@ public static class DnsmasqConfIncludeParser
|
||||
private static string[] GetLines(string path, IReadOnlyDictionary<string, string[]> pathToLines) =>
|
||||
pathToLines.TryGetValue(path, out var L) ? L : Array.Empty<string>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ordered list of absolute config file paths dnsmasq loads, in the exact order
|
||||
/// dnsmasq reads them (main interleaved with conf-file and conf-dir).
|
||||
@@ -71,7 +73,7 @@ public static class DnsmasqConfIncludeParser
|
||||
if (!seenPaths.Add(canonicalPath))
|
||||
yield break; // dnsmasq skips re-reading same file (by inode; we use path)
|
||||
|
||||
var lines = File.ReadAllLines(currentFilePath);
|
||||
var lines = DnsmasqFileEncoding.ReadConfigLines(currentFilePath);
|
||||
var dir = Path.GetDirectoryName(canonicalPath) ?? "";
|
||||
|
||||
// Yield "file started" so callers (e.g. GetIncludedPathsWithSource) can add this file even if it only has conf-file/conf-dir lines.
|
||||
@@ -100,6 +102,7 @@ public static class DnsmasqConfIncludeParser
|
||||
var (directory, matchSuffix, ignoreSuffix) = ParseConfDirValue(value);
|
||||
if (string.IsNullOrEmpty(directory)) continue;
|
||||
var resolvedDir = ResolvePath(directory, dir) ?? directory;
|
||||
resolvedDir = Path.GetFullPath(resolvedDir);
|
||||
if (!Directory.Exists(resolvedDir)) continue;
|
||||
var files = GetConfDirFilesSorted(resolvedDir, matchSuffix, ignoreSuffix);
|
||||
foreach (var f in files)
|
||||
@@ -191,7 +194,7 @@ public static class DnsmasqConfIncludeParser
|
||||
if (!File.Exists(mainFull))
|
||||
return null;
|
||||
|
||||
foreach (var line in File.ReadAllLines(mainFull))
|
||||
foreach (var line in DnsmasqFileEncoding.ReadConfigLines(mainFull))
|
||||
{
|
||||
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
|
||||
if (kv == null)
|
||||
+1
-1
@@ -3,7 +3,7 @@ using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a single line of the dnsmasq DHCPv4 lease file (e.g. /var/lib/misc/dnsmasq.leases).
|
||||
+1
-1
@@ -3,7 +3,7 @@ using Superpower;
|
||||
using Superpower.Model;
|
||||
using Superpower.Parsers;
|
||||
|
||||
namespace DnsmasqWebUI.Parsers;
|
||||
namespace DnsmasqWebUI.Infrastructure.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a single line of an /etc/hosts-style file. Format: IP_address canonical_hostname [aliases...]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for application hosted services that are registered via assembly scanning.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Marker for types that are registered as their own concrete type with scoped lifetime.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for application services that are registered as scoped.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for application services that are registered as singleton.
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
using DnsmasqWebUI.Client.Models;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Client;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves client-side preferences (e.g. polling intervals) to browser local storage.
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for the config set (main + conf-dir + managed file). Invalidates on file watchers, staleness, or manual Invalidate.
|
||||
@@ -0,0 +1,23 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>Reads and writes the managed dnsmasq config file and DHCP hosts entries (the app-managed config and dhcp-host= lines).</summary>
|
||||
public interface IDnsmasqConfigService : IApplicationScopedService
|
||||
{
|
||||
/// <summary>Reads DHCP host entries from the managed config (dhcp-host= lines).</summary>
|
||||
Task<IReadOnlyList<DhcpHostEntry>> ReadDhcpHostsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Writes DHCP host entries to the managed config file (replaces dhcp-host= lines).</summary>
|
||||
Task WriteDhcpHostsAsync(IReadOnlyList<DhcpHostEntry> entries, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Reads the full managed config file as structured lines plus the effective addn-hosts path in file (for display).</summary>
|
||||
Task<ManagedConfigContent> ReadManagedConfigAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Writes the full managed config file from the given lines (round-trip from ReadManagedConfigAsync).</summary>
|
||||
Task WriteManagedConfigAsync(IReadOnlyList<DnsmasqConfLine> lines, CancellationToken ct = default);
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>Discovers the dnsmasq config set (main + conf-file/conf-dir) and the managed file path.</summary>
|
||||
public interface IDnsmasqConfigSetService : IApplicationSingleton
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Lookup for custom display fragments per effective-config field (section + option).
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers, staleness, or Invalidate.
|
||||
@@ -0,0 +1,13 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>Reads and writes the app-managed hosts file (the single hosts file the app edits).</summary>
|
||||
public interface IHostsFileService : IApplicationScopedService
|
||||
{
|
||||
/// <summary>Reads all entries from the managed hosts file (including comments and passthrough lines).</summary>
|
||||
Task<IReadOnlyList<HostEntry>> ReadAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Writes the given entries to the managed hosts file (replaces file content).</summary>
|
||||
Task WriteAsync(IReadOnlyList<HostEntry> entries, CancellationToken ct = default);
|
||||
}
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for the leases file; invalidates when the file changes (e.g. via file watcher).
|
||||
@@ -10,5 +10,6 @@ public interface ILeasesCache : IApplicationSingleton
|
||||
/// <summary>Forces the next <see cref="GetOrRefreshAsync"/> to re-read the file. Use for manual Refresh; no need to recreate the file watcher.</summary>
|
||||
void Invalidate();
|
||||
|
||||
/// <summary>Returns cached lease entries, or re-reads from disk if invalidated or file changed.</summary>
|
||||
Task<(bool Available, IReadOnlyList<LeaseEntry>? Entries)> GetOrRefreshAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>Reads the dnsmasq DHCP leases file (path from effective config).</summary>
|
||||
public interface ILeasesFileService : IApplicationScopedService
|
||||
{
|
||||
/// <summary>Reads lease entries from the leases file. Throws if file is not configured or not readable.</summary>
|
||||
Task<IReadOnlyList<LeaseEntry>> ReadAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Attempts to read the leases file. Returns (false, null) when not configured or not readable; (true, entries) otherwise.</summary>
|
||||
Task<(bool Available, IReadOnlyList<LeaseEntry>? Entries)> TryReadAsync(CancellationToken ct = default);
|
||||
}
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>Runs a shell command with timeout and cancellation. Used for status, reload, status show, and logs commands.</summary>
|
||||
public interface IProcessRunner : IApplicationScopedService
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
/// <summary>Runs the configured dnsmasq reload command (e.g. systemctl reload dnsmasq).</summary>
|
||||
public interface IReloadService : IApplicationScopedService
|
||||
{
|
||||
/// <summary>Executes the reload command and returns exit code and output. Serialised so only one reload runs at a time.</summary>
|
||||
Task<ReloadResult> ReloadAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>Result of running the reload command.</summary>
|
||||
/// <param name="Success">True when the command exited with code 0.</param>
|
||||
/// <param name="ExitCode">Process exit code; -1 when failed to start or timed out.</param>
|
||||
/// <param name="StdOut">Standard output from the command.</param>
|
||||
/// <param name="StdErr">Standard error from the command.</param>
|
||||
public record ReloadResult(bool Success, int ExitCode, string? StdOut, string? StdErr);
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
using System.Text.Json;
|
||||
using DnsmasqWebUI.Client.Models;
|
||||
using DnsmasqWebUI.Client.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Client;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace DnsmasqWebUI.Client.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and saves client-side settings to browser local storage via JS interop.
|
||||
+8
-7
@@ -1,13 +1,14 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for the config set (main + conf-dir + managed file). Reads all files once per refresh,
|
||||
@@ -203,7 +204,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
||||
}
|
||||
try
|
||||
{
|
||||
dict[canonical] = File.ReadAllLines(path, Encoding.UTF8);
|
||||
dict[canonical] = DnsmasqFileEncoding.ReadConfigLines(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
+10
-19
@@ -1,12 +1,14 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Infrastructure;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
{
|
||||
@@ -113,7 +115,9 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var read = await File.ReadAllLinesAsync(path, DnsmasqFileEncoding.Utf8NoBom, ct);
|
||||
rawLines = StripBomFromFirstLine(read);
|
||||
var list = read.ToList();
|
||||
DnsmasqFileEncoding.StripBomFromFirstLine(list);
|
||||
rawLines = list;
|
||||
}
|
||||
else
|
||||
rawLines = Array.Empty<string>();
|
||||
@@ -203,17 +207,4 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
_logger.LogInformation("Wrote managed config file: {Path}", path);
|
||||
}
|
||||
|
||||
/// <summary>Strip UTF-8 BOM from first line if present.</summary>
|
||||
private static IReadOnlyList<string> StripBomFromFirstLine(IReadOnlyList<string> lines)
|
||||
{
|
||||
if (lines.Count == 0) return lines;
|
||||
var first = lines[0];
|
||||
if (first.Length > 0 && first[0] == '\uFEFF')
|
||||
{
|
||||
var list = lines.ToList();
|
||||
list[0] = first[1..];
|
||||
return list;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
public class DnsmasqConfigSetService : IDnsmasqConfigSetService
|
||||
{
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Maps (sectionId, optionName) to custom effective-config value display component types and builds
|
||||
+6
-16
@@ -1,13 +1,12 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Infrastructure;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.EffectiveConfig;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Runs once at startup: ensures the managed file is included only by a conf-file= directive (never conf-dir=),
|
||||
@@ -52,7 +51,7 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
|
||||
var lines = File.Exists(mainFull)
|
||||
? (await File.ReadAllLinesAsync(mainFull, DnsmasqFileEncoding.Utf8NoBom, cancellationToken)).ToList()
|
||||
: new List<string>();
|
||||
StripBomFromFirstLine(lines);
|
||||
DnsmasqFileEncoding.StripBomFromFirstLine(lines);
|
||||
|
||||
// Remove any conf-file= line that points to our managed path (we will add it as the last line).
|
||||
for (var i = lines.Count - 1; i >= 0; i--)
|
||||
@@ -96,14 +95,5 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
|
||||
await configService.WriteManagedConfigAsync(Array.Empty<DnsmasqConfLine>(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Strip UTF-8 BOM from first line if present, so we do not perpetuate it when writing.</summary>
|
||||
private static void StripBomFromFirstLine(List<string> lines)
|
||||
{
|
||||
if (lines.Count == 0) return;
|
||||
var first = lines[0];
|
||||
if (first.Length > 0 && first[0] == '\uFEFF')
|
||||
lines[0] = first[1..];
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
+5
-4
@@ -1,11 +1,12 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers (managed + system path), staleness, or Invalidate.
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
using DnsmasqWebUI.Infrastructure;
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
public class HostsFileService : IHostsFileService
|
||||
{
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Parsers;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Parsers;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton cache for the leases file with a <see cref="FileSystemWatcher"/> so we re-read only when the file changes.
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
public class LeasesFileService : ILeasesFileService
|
||||
{
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Contracts;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
/// <summary>Runs shell commands via /bin/sh with async output capture and timeout. Used by StatusController and ReloadService.</summary>
|
||||
public sealed class ProcessRunner : IProcessRunner
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
using DnsmasqWebUI.Configuration;
|
||||
using DnsmasqWebUI.Services.Abstractions;
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Services;
|
||||
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||
|
||||
public class ReloadService : IReloadService
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Client.Models;
|
||||
namespace DnsmasqWebUI.Models.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side preferences persisted in browser local storage.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Client.Models;
|
||||
namespace DnsmasqWebUI.Models.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Which subset of client settings to show in the settings modal.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Configuration;
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Application-level settings (title, etc.). Bound from "Application" config section
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace DnsmasqWebUI.Configuration;
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for dnsmasq paths and reload/status commands.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DnsmasqWebUI.Configuration;
|
||||
namespace DnsmasqWebUI.Models.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Validates required dnsmasq options at startup. If config is missing or default paths don't point at existing files,
|
||||
@@ -0,0 +1,19 @@
|
||||
using DnsmasqWebUI.Models.Dhcp;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Contracts;
|
||||
|
||||
/// <summary>Immutable snapshot from the config set cache: config set, effective config, sources, managed file content, and DHCP host entries (one read per refresh).</summary>
|
||||
/// <param name="Set">Ordered config set (main + conf-file/conf-dir) and managed file paths.</param>
|
||||
/// <param name="Config">Effective dnsmasq config built from all files.</param>
|
||||
/// <param name="Sources">Source per field (file path, readonly) for UI tooltips.</param>
|
||||
/// <param name="ManagedContent">Parsed lines and effective addn-hosts path of the managed config file.</param>
|
||||
/// <param name="DhcpHostEntries">DHCP host entries read from the managed file.</param>
|
||||
public record ConfigSetSnapshot(
|
||||
DnsmasqConfigSet Set,
|
||||
EffectiveDnsmasqConfig Config,
|
||||
EffectiveConfigSources Sources,
|
||||
ManagedConfigContent ManagedContent,
|
||||
IReadOnlyList<DhcpHostEntry> DhcpHostEntries
|
||||
);
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
using DnsmasqWebUI.Models.Hosts;
|
||||
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Models.Contracts;
|
||||
|
||||
/// <summary>Immutable snapshot from the hosts cache: managed hosts file entries and read-only hosts files (system + addn-hosts).</summary>
|
||||
/// <param name="ManagedEntries">Entries from the app-managed hosts file.</param>
|
||||
/// <param name="ReadOnlyFiles">Read-only hosts files (system hosts when configured, then addn-hosts that are not the managed file).</param>
|
||||
public record HostsSnapshot(
|
||||
IReadOnlyList<HostEntry> ManagedEntries,
|
||||
IReadOnlyList<ReadOnlyHostsFile> ReadOnlyFiles
|
||||
@@ -0,0 +1,8 @@
|
||||
using DnsmasqWebUI.Models.Config;
|
||||
|
||||
namespace DnsmasqWebUI.Models.Contracts;
|
||||
|
||||
/// <summary>Full managed file content. EffectiveHostsPathInFile is parsed from AddnHosts line if present (for display only).</summary>
|
||||
/// <param name="Lines">Parsed config lines (blank, comment, addn-hosts, dhcp-host, other).</param>
|
||||
/// <param name="EffectiveHostsPathInFile">Path from addn-hosts= in the managed file, for display; empty when not set.</param>
|
||||
public record ManagedConfigContent(IReadOnlyList<DnsmasqConfLine> Lines, string EffectiveHostsPathInFile);
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
namespace DnsmasqWebUI.Services.Abstractions;
|
||||
namespace DnsmasqWebUI.Models.Contracts;
|
||||
|
||||
/// <summary>Result of running a shell command via <see cref="IProcessRunner"/>.</summary>
|
||||
/// <summary>Result of running a shell command via IProcessRunner.</summary>
|
||||
/// <param name="ExitCode">Process exit code, or null if timed out or failed to start.</param>
|
||||
/// <param name="Stdout">Standard output.</param>
|
||||
/// <param name="Stderr">Standard error.</param>
|
||||
@@ -1,21 +1,42 @@
|
||||
namespace DnsmasqWebUI.Models.Dhcp;
|
||||
|
||||
/// <summary>One dhcp-host= line (MAC(s), optional address, name, lease, options). Used for GET/PUT api/dhcp/hosts.</summary>
|
||||
public class DhcpHostEntry
|
||||
{
|
||||
/// <summary>1-based line number in the config file. Used for display and matching.</summary>
|
||||
public int LineNumber { get; set; }
|
||||
|
||||
/// <summary>Stable identifier (content-based: MACs|Address|Name, with ":LineNumber" for uniqueness). Set by server on GET; used to match entries on PUT so reordering is safe.</summary>
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
/// <summary>Original line text as read from the config (for passthrough and round-trip).</summary>
|
||||
public string RawLine { get; set; } = "";
|
||||
|
||||
/// <summary>True when the line is a comment (starts with #).</summary>
|
||||
public bool IsComment { get; set; }
|
||||
|
||||
/// <summary>True when the entry was marked for deletion in the UI (removed on save).</summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
/// <summary>True when the line could not be parsed (preserved as RawLine on write).</summary>
|
||||
public bool Ignore { get; set; }
|
||||
|
||||
/// <summary>MAC address(es) from the dhcp-host line. One or more; order preserved.</summary>
|
||||
public List<string> MacAddresses { get; set; } = new();
|
||||
|
||||
/// <summary>Hostname or DHCP hostname from the line; null when not set.</summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>Reserved IP address from the line; null when not set.</summary>
|
||||
public string? Address { get; set; }
|
||||
|
||||
/// <summary>Lease identifier (e.g. "01:02:03:04:05:06"); null when not set.</summary>
|
||||
public string? Lease { get; set; }
|
||||
|
||||
/// <summary>Extra tokens after the main dhcp-host fields (e.g. set:name); preserved on write.</summary>
|
||||
public List<string> Extra { get; set; } = new();
|
||||
|
||||
/// <summary>Inline comment text from the line; null when none.</summary>
|
||||
public string? Comment { get; set; }
|
||||
|
||||
/// <summary>True when this entry is from the managed file (editable). False when from main config or another file (read-only). Set by server on GET; new entries created in the UI should set this to true.</summary>
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
namespace DnsmasqWebUI.Models.Dhcp;
|
||||
|
||||
/// <summary>One DHCP lease entry from the dnsmasq leases file (timestamp, MAC, IP, hostname, client-id).</summary>
|
||||
public class LeaseEntry
|
||||
{
|
||||
/// <summary>Lease expiry time as Unix timestamp (seconds since epoch).</summary>
|
||||
public long Epoch { get; set; }
|
||||
|
||||
/// <summary>Client MAC address.</summary>
|
||||
public string Mac { get; set; } = "";
|
||||
|
||||
/// <summary>Assigned IP address.</summary>
|
||||
public string Address { get; set; } = "";
|
||||
|
||||
/// <summary>Hostname from DHCP or DNS; empty when unknown.</summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>DHCP client identifier; empty when not set.</summary>
|
||||
public string ClientId { get; set; } = "";
|
||||
|
||||
/// <summary>Lease expiry as DateTime (derived from Epoch).</summary>
|
||||
public DateTime Timestamp => DateTimeOffset.FromUnixTimeSeconds(Epoch).DateTime;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user