using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
///
/// L13-1: DNS 레코드 조회 핸들러. "dns" 프리픽스로 사용합니다.
///
/// 예: dns google.com → A/AAAA 레코드 조회
/// dns google.com mx → MX 레코드
/// dns google.com txt → TXT 레코드
/// dns google.com ns → NS 레코드
/// dns 8.8.8.8 → PTR(역방향) 조회
/// Enter → 결과를 클립보드에 복사.
///
/// ⚠ 사내 모드: 내부 호스트만 조회 허용.
///
public class DnsQueryHandler : IActionHandler
{
public string? Prefix => "dns";
public PluginMetadata Metadata => new(
"DNS",
"DNS 레코드 조회 — A · AAAA · MX · TXT · NS · PTR",
"1.0",
"AX");
public Task> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem("DNS 레코드 조회",
"예: dns google.com / dns google.com mx / dns 8.8.8.8",
null, null, Symbol: "\uE968"));
items.Add(new LauncherItem("dns localhost", "로컬 A 레코드", null, null, Symbol: "\uE968"));
items.Add(new LauncherItem("dns example.com mx", "MX 레코드", null, null, Symbol: "\uE968"));
items.Add(new LauncherItem("dns example.com txt","TXT 레코드", null, null, Symbol: "\uE968"));
return Task.FromResult>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var host = parts[0];
var recType = parts.Length > 1 ? parts[1].ToUpperInvariant() : "A";
// 사내 모드 확인
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
var isInternal = settings?.InternalModeEnabled ?? true;
if (isInternal && !IsInternalHost(host))
{
items.Add(new LauncherItem(
"사내 모드 제한",
$"'{host}'은 외부 호스트입니다. 설정에서 사외 모드를 활성화하세요.",
null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
items.Add(new LauncherItem(
$"{host} [{recType}]",
"Enter를 눌러 조회 실행",
null,
("query", $"{host}|{recType}"),
Symbol: "\uE968"));
// 레코드 타입 빠른 선택 힌트
if (parts.Length == 1)
{
foreach (var t in new[] { "A", "AAAA", "MX", "TXT", "NS" })
items.Add(new LauncherItem($"dns {host} {t}", $"{t} 레코드 조회", null,
("query", $"{host}|{t}"), Symbol: "\uE968"));
}
return Task.FromResult>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
NotificationService.Notify("DNS", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
return;
}
if (item.Data is not ("query", string queryData)) return;
var idx = queryData.IndexOf('|');
var host = queryData[..idx];
var recType = queryData[(idx + 1)..];
NotificationService.Notify("DNS", $"{host} [{recType}] 조회 중…");
try
{
var results = await QueryDnsAsync(host, recType, ct);
if (results.Count == 0)
{
NotificationService.Notify("DNS", $"{host} [{recType}] — 레코드 없음");
return;
}
var summary = string.Join("\n", results);
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(summary));
NotificationService.Notify("DNS", $"{results.Count}개 레코드 조회됨 · 클립보드 복사");
}
catch (OperationCanceledException)
{
NotificationService.Notify("DNS", "조회 취소됨");
}
catch (Exception ex)
{
NotificationService.Notify("DNS", $"오류: {ex.Message}");
}
}
// ── DNS 조회 ─────────────────────────────────────────────────────────────
private static async Task> QueryDnsAsync(string host, string type, CancellationToken ct)
{
return type switch
{
"A" or "AAAA" => await QueryAAsync(host, type, ct),
"PTR" => await QueryPtrAsync(host, ct),
_ => await QueryViaNslookupAsync(host, type, ct),
};
}
private static async Task> QueryAAsync(string host, string type, CancellationToken ct)
{
var family = type == "AAAA" ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork;
var addrs = await Dns.GetHostAddressesAsync(host, family, ct);
return addrs.Select(a => a.ToString()).ToList();
}
private static async Task> QueryPtrAsync(string ip, CancellationToken ct)
{
if (!IPAddress.TryParse(ip, out _))
return [$"'{ip}'은 유효한 IP 주소가 아닙니다"];
try
{
var entry = await Dns.GetHostEntryAsync(ip, ct);
return [entry.HostName];
}
catch
{
return [$"PTR 레코드 없음: {ip}"];
}
}
/// MX/TXT/NS/CNAME: nslookup 프로세스 실행으로 조회
private static async Task> QueryViaNslookupAsync(string host, string type, CancellationToken ct)
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "nslookup",
Arguments = $"-type={type} {host}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
};
using var proc = new System.Diagnostics.Process { StartInfo = psi };
proc.Start();
var stdout = await proc.StandardOutput.ReadToEndAsync(ct);
await proc.WaitForExitAsync(ct);
return ParseNslookupOutput(stdout, type);
}
private static List ParseNslookupOutput(string output, string type)
{
var results = new List();
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
// 서버 응답 헤더 건너뜀 (첫 2줄)
var skip = true;
foreach (var line in lines)
{
var trimmed = line.Trim();
if (skip)
{
if (trimmed.StartsWith("Non-authoritative", StringComparison.OrdinalIgnoreCase) ||
trimmed.StartsWith("Name:", StringComparison.OrdinalIgnoreCase) ||
(type == "MX" && trimmed.Contains("mail exchanger")) ||
(type == "TXT" && trimmed.Contains("text =")) ||
(type == "NS" && trimmed.Contains("nameserver")))
skip = false;
else
continue;
}
if (string.IsNullOrWhiteSpace(trimmed)) continue;
// MX: "... mail exchanger = 10 aspmx.l.google.com"
if (type == "MX" && trimmed.Contains("mail exchanger"))
{
var idx = trimmed.IndexOf('=');
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
continue;
}
// TXT: "... text = "v=spf1 …""
if (type == "TXT" && trimmed.Contains("text ="))
{
var idx = trimmed.IndexOf("text =", StringComparison.OrdinalIgnoreCase);
if (idx >= 0) results.Add(trimmed[(idx + 6)..].Trim().Trim('"'));
continue;
}
// NS: "nameserver = ns1.google.com"
if (type == "NS" && trimmed.Contains("nameserver"))
{
var idx = trimmed.IndexOf('=');
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
continue;
}
// CNAME: "canonical name = …"
if (type == "CNAME" && trimmed.Contains("canonical name"))
{
var idx = trimmed.IndexOf('=');
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
continue;
}
// Address: 주소 행
if (trimmed.StartsWith("Address:", StringComparison.OrdinalIgnoreCase))
{
var idx = trimmed.IndexOf(':');
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
}
}
return results.Count > 0 ? results : [$"조회 결과 없음 ({type})"];
}
private static bool IsInternalHost(string host)
{
if (host is "localhost" or "127.0.0.1") return true;
if (IPAddress.TryParse(host, out var addr))
{
var s = addr.ToString();
return s.StartsWith("192.168.") || s.StartsWith("10.") ||
System.Text.RegularExpressions.Regex.IsMatch(s,
@"^172\.(1[6-9]|2\d|3[01])\.");
}
// 도메인 이름은 사내 모드에서 외부로 간주
return false;
}
}