mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-22 07:52:08 +10:00
- Effective config: track value sources (ConfigValueSource, EffectiveConfigSources) and show readonly badges/tooltips for non-managed files. Multi-value options use ValueWithSource/PathWithSource for JSON. StatusClient uses camelCase serialization for reliable deserialization. - Parsing: align with dnsmasq (conf-file/conf-dir order, suffix filter, case- sensitive keys). DnsmasqConfIncludeParser and DnsmasqConfigSetService build effective config and sources. New DnsmasqConfigSetServiceTests. - UI: rename Overview/Home to Dnsmasq (page, nav, doc comments). Add EffectiveConfigOptionItem and EffectiveConfigMultiValueRow components. Dnsmasq.razor uses them; Hosts.razor shows addn-hosts with source. Encode server-derived strings in markup to avoid XSS. - Home section namespace: Components.Home -> Components.Dnsmasq (LogsSection, StatusSection, _Imports). - Test data: expand testdata (README, resolv.dnsmasq, hosts.extra, dnsmasq.d configs). dhcp.conf static IP aligned with testnet. - agents.md: add agent instructions for test harness and conventions.
165 lines
6.2 KiB
Plaintext
165 lines
6.2 KiB
Plaintext
@page "/hosts"
|
|
@rendermode InteractiveServer
|
|
@namespace DnsmasqWebUI.Components.Pages
|
|
@inject IStatusClient StatusClient
|
|
@inject IHostsClient HostsClient
|
|
@inject IOptions<ApplicationOptions> AppOptions
|
|
<PageTitle>Hosts | @AppTitle</PageTitle>
|
|
|
|
<h1 class="page-title">Hosts file</h1>
|
|
|
|
@if (_status == null)
|
|
{
|
|
<p class="d-flex align-items-center gap-2">
|
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
Loading...
|
|
</p>
|
|
}
|
|
else if (_status.NoHosts && !_pathInAddnHosts)
|
|
{
|
|
<p class="text-muted">Hosts editing is disabled: <strong>no-hosts</strong> is set in dnsmasq config (dnsmasq only uses addn-hosts files) and the configured path is not in the effective addn-hosts list, so dnsmasq would not read the file.</p>
|
|
@if (_status.AddnHostsPaths?.Count > 0)
|
|
{
|
|
<p class="text-muted small">Effective addn-hosts paths: @RenderAddnHostsList()</p>
|
|
}
|
|
}
|
|
else if (string.IsNullOrEmpty(_status.SystemHostsPath))
|
|
{
|
|
<p class="text-muted">No system hosts file configured. Set <strong>Dnsmasq:SystemHostsPath</strong> (e.g. /etc/hosts) to edit a hosts file from this app.</p>
|
|
@if (_status.AddnHostsPaths?.Count > 0)
|
|
{
|
|
<p class="text-muted small">Dnsmasq loads these addn-hosts files: @RenderAddnHostsList()</p>
|
|
}
|
|
}
|
|
else if (!_status.SystemHostsPathExists)
|
|
{
|
|
<p class="text-warning">System hosts file not found: @_status.SystemHostsPath</p>
|
|
}
|
|
else
|
|
{
|
|
@if (_status.DnsmasqStatus == "inactive" || _status.DnsmasqStatus == "unknown")
|
|
{
|
|
<div class="alert alert-warning">Dnsmasq service appears to be <strong>@_status.DnsmasqStatus</strong>. Save will update the file but reload may fail until the service is fixed.</div>
|
|
}
|
|
@if (_error != null)
|
|
{
|
|
<div class="alert alert-danger">@_error</div>
|
|
}
|
|
@if (_message != null)
|
|
{
|
|
<div class="alert alert-success">@_message</div>
|
|
}
|
|
<div class="table-responsive-cards">
|
|
<div class="d-none d-md-block">
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr><th>#</th><th>Address</th><th>Names</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var e in _entries.Where(x => !x.IsPassthrough))
|
|
{
|
|
<tr class="@(e.IsComment ? "table-secondary" : "")" @key="e.Id">
|
|
<td>@e.LineNumber</td>
|
|
<td>@e.Address</td>
|
|
<td>@string.Join(", ", e.Names)</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="d-md-none table-cards-mobile">
|
|
@foreach (var e in _entries.Where(x => !x.IsPassthrough))
|
|
{
|
|
<div class="card mb-2 @(e.IsComment ? "table-secondary" : "")" @key="e.Id">
|
|
<div class="card-body py-2">
|
|
<div class="table-card-row"><span class="table-card-label">#</span> @e.LineNumber</div>
|
|
<div class="table-card-row"><span class="table-card-label">Address</span> @e.Address</div>
|
|
<div class="table-card-row"><span class="table-card-label">Names</span> @string.Join(", ", e.Names)</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<p class="text-muted small">Path: @_status.SystemHostsPath</p>
|
|
<button class="btn btn-primary mt-2" @onclick="Save" disabled="@_saving">Save</button>
|
|
}
|
|
|
|
@code {
|
|
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
|
|
private DnsmasqServiceStatus? _status;
|
|
private List<HostEntry> _entries = new();
|
|
private string? _error;
|
|
private string? _message;
|
|
private bool _saving;
|
|
|
|
/// <summary>True when SystemHostsPath is in the effective addn-hosts list (editing allowed when no-hosts is set).</summary>
|
|
private bool _pathInAddnHosts => _status != null
|
|
&& !string.IsNullOrEmpty(_status.SystemHostsPath)
|
|
&& _status.AddnHostsPaths?.Any(p => string.Equals(p, Path.GetFullPath(_status.SystemHostsPath!.Trim()), StringComparison.Ordinal)) == true;
|
|
|
|
/// <summary>Returns MarkupString so Blazor renders raw HTML (path + source label).</summary>
|
|
private MarkupString RenderAddnHostsList()
|
|
{
|
|
if (_status?.AddnHostsPaths == null || _status.AddnHostsPaths.Count == 0)
|
|
return (MarkupString)"(none)";
|
|
var srcList = _status.EffectiveConfigSources?.AddnHostsPaths;
|
|
if (srcList != null && srcList.Count == _status.AddnHostsPaths.Count)
|
|
{
|
|
return (MarkupString)string.Join(", ", srcList.Select(e =>
|
|
{
|
|
var src = e.Source;
|
|
var tip = src?.GetReadOnlyTooltip() ?? "";
|
|
var label = src == null ? "?" : (src.IsReadOnly ? "from " + src.FileName + ", readonly" : "editable");
|
|
return $"{System.Net.WebUtility.HtmlEncode(e.Path)} <span class=\"text-muted\" title=\"{System.Net.WebUtility.HtmlEncode(tip)}\">({System.Net.WebUtility.HtmlEncode(label)})</span>";
|
|
}));
|
|
}
|
|
return (MarkupString)System.Net.WebUtility.HtmlEncode(string.Join(", ", _status.AddnHostsPaths));
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await Load();
|
|
}
|
|
|
|
private async Task Load()
|
|
{
|
|
_error = null;
|
|
_message = null;
|
|
try
|
|
{
|
|
_status = await StatusClient.GetStatusAsync();
|
|
var entries = await HostsClient.GetHostsAsync();
|
|
_entries = entries?.ToList() ?? new List<HostEntry>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_error = ex.Message;
|
|
}
|
|
}
|
|
|
|
private async Task Save()
|
|
{
|
|
_saving = true;
|
|
_error = null;
|
|
_message = null;
|
|
try
|
|
{
|
|
var result = await HostsClient.SaveHostsAsync(_entries);
|
|
if (result.Reload.Success)
|
|
_message = "Saved and dnsmasq reloaded.";
|
|
else if (!string.IsNullOrEmpty(result.Reload.StdErr))
|
|
_error = "Saved but dnsmasq reload failed: " + result.Reload.StdErr;
|
|
else
|
|
_message = "Saved.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_saving = false;
|
|
}
|
|
}
|
|
}
|